Shumë kthime të lumtura të hershme
Vendosa ta shkruaj këtë artikull sepse mbaj mend që kur fillova të mësoja Scala-n rreth viteve 2013–2014, problemi se si të kthehesha herët nga një lak më ka ndodhur në të vërtetë disa herë. Kisha një koleksion të cilin doja ta kaloja duke kërkuar hyrjen e parë që plotësonte disa kërkesa, dhe më pas doja të dilja nga cikli, duke marrë atë rezultat të përshtatshëm me vete. Në kohën kur punoja në Java në një ueb-aplikacion për menaxhimin e kohës dhe situata si ajo e përshkruar më sipër më ndodhnin mjaft shpesh: Kishte koleksione të gjata shënimesh se sa kohë një person i caktuar punonte në një projekt të caktuar në një ditë të caktuar, dhe bëra pyetje të ndërlikuara kundër tyre. "Gjeni një ndodhi të një dite kur ekipi punoi më shumë orë person-orë se X". "Gjeni një shembull të një defekti të cilit iu deshën më shumë se Y ditë për t'u rregulluar". Dhe kështu me radhë. Në Java, zakonisht po ndiqja të njëjtin model —mora një koleksion të hyrjeve origjinale (le t'i quajmë ato foos
të llojit Foo
), kam kryer një konvertim nganjëherë mjaft të shtrenjtë në një hyrje të prejardhur (një bar
i llojit Bar
), më pas bëri një vërtetim gjithashtu nganjëherë mjaft kompleks të atij bar
, duke kontrolluar nëse i plotëson kërkesat, dhe nëse po, atëherë e ktheva. Nëse nuk gjeja asgjë, ktheva null
.
Bar complexConversion(Foo foo) { ... }
bool complexValidation(Bar bar) { ... }
Bar findFirstValidBar(Collection<Foo> foos) { for(Foo foo : foos) { Bar bar = complexConversion(foo) if (complexValidation(bar)) return bar } return null }
Qasja imperative
Natyrisht, hapat e mi të parë në Scala si zhvillues Java ishin thjesht përkthimi i kodit Java në Scala pothuajse një me një, fjalë për fjalë:
def complexConversion(foo: Foo): Bar = ... def complexValidation(bar: Bar): Boolean = ...
def findFirstValidBar(seq: Seq[Foo]): Option[Bar] = { for (foo <- seq) { val bar = complexConversion(foo) if (complexValidation(bar)) return Some(bar) } None }
I vetmi ndryshim i vërtetë këtu është se unë shmang null
dhe përdor një Option
. Për të gjitha qëllimet tona praktike këtu, një Option
është një koleksion që mund të përbëhet nga zero ose një element. Është ose Some(bar)
, dhe më vonë mund ta nxjerr atë bar
prej tij, ose është None
.
Por ky nuk është kod i mirë Scala, larg tij. Fjala kyçe return
, edhe pse ekziston në Scala, dekurajohet fuqimisht. Në shumicën e rasteve, zhvilluesit përdorin return
si deklaratën e fundit në një bllok. Në Scala, çdo bllok kodi është një shprehje e cila automatikisht kthen rezultatin e shprehjes së fundit në bllok. Pra, në vend të return x
ju thjesht mund të shkruani x
në rreshtin e fundit, por më shpesh as nuk e bëni këtë fakti që gjithçka është një shprehje ju jep fuqinë për të rirenditur të gjithë bllokun e kodoni në mënyra që në Java do të dukeshin shumë të ngathët dhe do të përfundonin me kod shumë më konciz Scala. Kjo eliminon 90% të kthimeve. Sa për 10% të tjera, pra kthimet e hershme nga loop-et kryesisht, mirë... Kthimet e hershme konsiderohen si një erë kodi. Ata e bëjnë një sekuencë shprehjesh më pak të lexueshme sepse programuesi nuk mund të mbështetet më në atë që deklarata e fundit është ajo që kthen rezultatin nga blloku. Nëse është e mundur, duhet ta rirenditni kodin në atë mënyrë që return
të mos jetë më e nevojshme.
Por kjo nuk është aq e lehtë në këtë rast. Fillimisht mësova disa gjysmë zgjidhje për ta kapërcyer atë, më në fund mësova metodën e duhur, pastaj harrova gjithçka, sepse në atë kohë kisha aq shumë për të mësuar në të njëjtën kohë sa që më la trurin për të lënë vend. për koncepte të tjera të reja, dhe vetëm kohët e fundit m'u kujtua se kodi që po shkruaj herë pas here është në fakt një ekuivalent FP i atij kthimi të hershëm të Java-s së vjetër. Dhe prandaj ky artikull. Përgjigja është…
Hapat e bebes
… do ta arrijmë hap pas hapi. Ose thjesht mund të kaloni paragrafët e ardhshëm nëse dëshironi. Por unë do të doja t'ju ftoja të kaloni me mua disa versione të ndërmjetme të metodës findFirstValidBar
, në mënyrë që në fund të kuptoni më mirë se pse zgjidhja përfundimtare duket si duket dhe çfarë më shumë mund të bëni me atë.
Së pari, le të shkojmë vetëm përgjatë vijës së rezistencës më të vogël: Çdo koleksion standard në Scala përcakton metodat find
dhe map
. find
kthen elementin e parë të koleksionit që plotëson një kallëzues si opsion do të kthejë None
nëse asnjë element nuk i plotëson kërkesat. map
konverton një koleksion elementësh të një lloji në një koleksion elementësh të një lloji tjetër. Dhe meqenëse Option
është gjithashtu një koleksion, ne mund ta përdorim atë këtu dhe kështu ta kthejmë të gjithë metodën findFirstValidBar
në këtë:
def findFirstValidBar(seq: Seq[Foo]): Option[Bar] =
seq.find(foo => complexValidation(complexConversion(foo)))
.map(complexConversion)
Për çdo foo
në seq
, ne e konvertojmë atë në bar
, pastaj e vërtetojmë, dhe nëse e kalon vërtetimin, atëherë ndalojmë përsëritjen mbi seq
dhe kthehemi... mirë, find
kthen foo
origjinale, jo derivatin bar
, kështu që ne duhet ta marrim atë foo
dhe ta konvertojmë përsëri përpara se të mund ta kthejmë atë. Nëse konvertimi është i parëndësishëm, ne mund ta shpërfillim këtë pengesë në fund të fundit, është vetëm një konvertim shtesë, ose zero nëse nuk kemi find
ndonjë element të vlefshëm por në përgjithësi, unë nuk ju pëlqen të qetësoheni me një zgjidhje nën-optimale, veçanërisht nëse një më e mirë nuk është edhe aq më komplekse.
Në bibliotekën standarde të koleksionit Scala ekziston një metodë e quajtur collect
e cila bashkon funksionalitetin e filter
dhe map
. Filtrimi i një koleksioni dhe më pas vendosja e rezultateve në një koleksion të diçkaje tjetër është aq e zakonshme sa ka një avantazh për të shkruar më shkurt. Në mënyrë të ngjashme, ekziston një metodë collectFirst
e cila bashkon find
dhe map
— në fund të fundit, find
është vetëm një filter
që ndalon pasi të gjejë elementin e parë të vlefshëm. Kështu që ne mund ta modifikojmë versionin nga lart dhe ta shkruajmë si:
def findFirstValidBar(seq: Seq[Foo]): Option[Bar] =
seq.collectFirst {
case foo if complexValidation(complexConversion(foo)) => complexConversion(foo)
}
Kllapat kaçurrelë dhe një vijë që fillon me shkronjat tregojnë se collectFirst
merr një argument një funksion të pjesshëm. Funksioni do të prodhojë një rezultat (complexConversion(foo)
) vetëm nëse plotësohet kushti (complexValidation(complexConversion(foo))
). Nëse rezulton rezultati, collectFirst
do të ndalojë përsëritjen mbi seq
dhe do ta kthejë atë. Përndryshe, do të provojë me një tjetër foo
.
unapply
Në vetvete, collectFirst
ende nuk e zgjidh problemin tonë. Për të kontrolluar nëse kushti është përmbushur, duhet të kryejmë një konvertim dhe një vërtetim, dhe më pas, nëse vërtetimi është i suksesshëm, duhet të konvertojmë edhe një herë origjinalin foo
, ashtu siç ndodhte në versionin me find
dhe map
. Por këtë herë kemi një sugjerim. Funksionet e pjesshme në Scala përfitojnë nga përputhja e modelit — ashtu si kur bëjmë match/case
, një rast në një funksion të pjesshëm mund të përdoret për të zbërthyer elementin foo
në diçka tjetër dhe atë mekanizëm dekonstruksioni, i njohur gjithashtu si Metoda unapply
, mund të përdoret si për konvertim ashtu edhe për vërtetim.
Por prisni, a nuk përdoret unapply
thjesht për të nxjerrë vlerat e fushës nga klasat e rasteve?
Jo.
object ValidBar {
def unapply(foo: Foo): Option[Bar] = {
val bar = complexConversion(foo)
if (complexValidation(bar)) Some(bar) else None
}
}
Praktikisht në çdo vend të kodit, ne mund të krijojmë një objekt të ri, ta quajmë atë diçka kuptimplotë (vini re se objektet nuk duhet të jenë gjithmonë objekte shoqëruese të klasave) dhe të implementojmë brenda tij një metodë unapply
e cila do të marrë një shembull të një lloji , dhe ktheni një Option
të një tjetri. Në rastin tonë, ajo merr një foo
, konvertoje atë, vërtetoje atë dhe kthen Some(bar)
nëse vërtetimi është i suksesshëm, ose None
përndryshe. Më pas, ne mund të përdorim metodën unapply
në përputhjen e modelit kudo, ashtu si kjo:
foo match {
case ValidBar(bar) => // do something with our valid bar
case _ => // ...
}
Pra, le ta përdorim atë në collectFirst
:
object ValidBar { def unapply(foo: Foo): Option[Bar] = { val bar = complexConversion(foo) if (complexValidation(bar)) Some(bar) else None } }
def findFirstValidBar(seq: Seq[Foo]): Option[Bar] = seq.collectFirst { case ValidBar(bar) => bar }
Fitore! tani kemi vetëm një konvertim, një vërtetim dhe e përfundojmë përsëritjen mbi seq
kur gjejmë elementin e parë të vlefshëm. Për më tepër, ne mund ta përdorim atë edhe në metodat collect
: seq.collect { case ValidBar(bar) => bar }
e thjeshtë do të japë përdorimin e të gjitha shiritave që kalojnë vërtetimin.
Megjithatë, ajo që mund të mos na pëlqejë në lidhje me këtë zgjidhje është folja e saj. Nuk është më një linjë. Në fakt, është edhe më i gjatë se versioni origjinal imperativ i findFirstValidBar
. Por kjo ndodh vetëm sepse kjo zgjidhje kërkon një kosto të caktuar në fillim: ne kemi nevojë për një objekt dhe një metodë unapply
edhe për aplikacionet më të thjeshta. Por ndërsa aplikacioni —, domethënë, konvertimet dhe vërtetimet — bëhen më komplekse, shpenzimet e përgjithshme bëhen më pak të rëndësishme. Na duhet vetëm të shkruajmë unapply
një herë, dhe më pas mund ta përdorim në collectFirst
kudo. Në fakt, ka një rast ku unapply
mund të na lejojë edhe të ruajmë disa rreshta.
Imagjinoni që konvertimi nga Foo
në Bar
mund të mos jetë gjithmonë i mundur. Në vend të def complexConversion(foo: Foo): Bar
, ne duhet të kemi def safeComplexConversion(foo: Foo): Option[Bar]
ku kthejmë Some(bar)
nëse konvertimi ishte i suksesshëm, dhe None
përndryshe. Tani versioni ynë imperativ i findFirstValidBar
duhet të duket diçka si kjo:
def safeComplexConversion(foo: Foo): Option[Bar] = ... def complexValidation(bar: Bar): Boolean = ...
def findFirstValidBar(seq: Seq[Foo]): Option[Bar] = { for (foo <- seq) safeComplexConversion(foo) match { case Some(bar) if complexValidation(bar) => return Some(bar) case _ => } None }
Është më komplekse dhe është e shëmtuar. Fjala kyçe return
është e vendosur jo vetëm në një lak për më, por edhe në një match/case
. Këtu përsëri ndeshemi me arsyen pse return
nuk duhet të përdoret. Në kodin Scala, shumë nivele të foleve janë mjaft të zakonshme — veçori si lambda, klasa anonime dhe përputhja e modelit, le ta bëjmë kodin më konciz. Por fjala kyçe return
në një kod të tillë e bën shumë më të vështirë leximin.
Siguria nga konvertimet e pamundura
Ndërkohë, duke qenë se metoda jonë unapply
merret me opsione, ndryshimet në versionin e fundit të findFirstValidBar
e bëjnë kodin edhe më të thjeshtë se më parë:
def safeComplexConversion(foo: Foo): Option[Bar] = ... def complexValidation(bar: Bar): Boolean = ...
object ValidBar { def unapply(foo: Foo): Option[Bar] = safeComplexConversion(foo).find(complexValidation) }
def findFirstValidBar(seq: Seq[Foo]): Option[Bar] = seq.collectFirst { case ValidBar(bar) => bar }
Përveç kësaj, metodat e konvertimit dhe të vërtetimit gjithashtu duhet të shkojnë diku - nëse ato përdoren gjithmonë së bashku, tani ato mund të vendosen pranë unapply
, brenda objektit. Do të thotë që nëse Foo
duhet të zbërthehet në disa mënyra në kodin tuaj - është mjaft e zakonshme që nga një strukturë origjinale e të dhënave ne mund të nxjerrim më shumë se një tip dytësor - do t'ju duhet vetëm të kujdeseni për emrat kuptimplotë të objekteve për ato derivatime:
object ValidBar { def convert(foo: Foo): Option[Bar] = ... def validate(bar: Bar): Boolean = ... def unapply(foo: Foo): Option[Bar] = convert(foo).find(validate) } // use as case ValidBar(bar) => ...
object ValidBaz { def convert(foo: Foo): Option[Baz] = ... def validate(baz: Baz): Boolean = ... def unapply(foo: Foo): Option[Baz] = convert(foo).find(validate) } // use as case ValidBaz(baz) => ...
object BarValidInADifferentWay { def convert(foo: Foo): Option[Bar] = ... def validate(bar: Bar): Boolean = ... def unapply(foo: Foo): Option[Bar] = convert(foo).find(validate) } // use as case BarValidInADifferentWay(bar) => ...
Kjo lejon ripërdorimin e konsiderueshëm të kodit. Jo vetëm që tani mund t'i përdorim ato metoda unapply
në collectFirst
dhe collect
, por gjithashtu në match/case
dhe pothuajse në çdo metodë nga biblioteka e koleksioneve Scala që pranon funksione të pjesshme. map
, flatMap
, foreach
- çfarëdo që ju nevojitet. Ne gjithashtu mund t'i kombinojmë ato në funksione të pjesshme nëse, le të themi, kërkojmë një foo që mund të konvertohet dhe vërtetohet në një mënyrë ose në një tjetër:
seq.collectFirst {
case ValidBar(bar) => bar
case BarValidInADifferentWay(bar) => bar
} // stops at the first element valid in any of those ways
Një tipar i përbashkët për të gjitha dekonstruksionet
Dhe nëse mësoheni me këtë model, mund të vini re se unapply
është gjithmonë i njëjtë dhe të nxirrni logjikën e përbashkët për një tipar:
// one such trait for the whole codebase trait Deconstruct[From, To] { def convert(from: From): Option[To] def validate(to: To): Boolean def unapply(from: From): Option[To] = convert(from).find(validate) }
// one for each implementation of convert and validate object ValidBar extends Deconstruct[Foo, Bar] { override def convert(foo: Foo): Option[Bar] = ... override def validate(bar: Bar): Boolean = ... }
// for each use def findFirstValidBar(seq: Seq[Foo]): Option[Bar] = seq.collectFirst { case ValidBar(bar) => bar }
Koleksione dembele
U soll në vëmendjen time pasi publikova versionin fillestar të këtij shënimi në blog se ekziston të paktën një mënyrë tjetër për të arritur të njëjtin efekt me koleksionet dembelë (faleminderit, Balmung-san!). Një koleksion dembel është një koleksion që në vend që të ketë elementët e tij tashmë të llogaritur dhe të gatshëm për qasje, ka një mënyrë për të llogaritur një element të caktuar kur është i nevojshëm. do të thotë që, duke qenë se kalojmë nëpër një koleksion vetëm derisa të gjejmë elementin e parë që plotëson disa kushte, dhe kur e gjejmë nuk jemi më të interesuar të qasemi në ndonjë tjetër, koleksioni nuk do të ekzekutojë metodat e konvertimit dhe të vërtetimit për ta.
Një koleksion i tillë dembel në Scala është Iterator. Ne mund të krijojmë një përsëritës nga Seq[Foo]
origjinale thjesht duke shkruar seq.iterator
. I gjithë kodi i nevojshëm për të gjetur një shirit të vlefshëm duket si ky:
def findFirstValidBar(seq: Seq[Foo]): Option[Bar] =
seq.iterator
.map(safeComplexConversion)
.find(_.exists(complexValidation))
.flatten
Megjithatë, ka disa shpenzime të përfshira. Është e pasigurt ta mbash përsëritësin përreth — duhet ta krijojmë çdo herë nga seq
origjinale. Gjithashtu, ripërdorimi i kodit zvogëlohet - nëse kemi më shumë se një mënyrë për të konvertuar dhe vërtetuar një element, do të na duhet të shkruajmë më shumë kod sesa në rastin e unapply
+ collectFirst
. Por në përgjithësi, ky është një shembull i mirë se si në Scala gjithçka mund të bëhet në më shumë se një mënyrë, në varësi të çfarë kompromisesh jemi të gatshëm të pranojmë.
Dhe kjo eshte. Faleminderit per leximin. Shpresoj që e gjithë kjo të jetë e dobishme për ju.
Disa lidhje:
- Një "thelb" me të gjithë shembujt.
- Një video (dhe shënimi i blogut shoqërues) rreth metodës
unapply
. - Një video tjetër dhe një shënim në blog për funksionet e pjesshme.
Fotografia e kopertinës e bërë nga Lucian nga Flickr, Creative Commons, disa të drejta të rezervuara.