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 fooseq, 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 FooBar 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 unapplycollectFirst 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:

Fotografia e kopertinës e bërë nga Lucian nga Flickr, Creative Commons, disa të drejta të rezervuara.