Pro­gram­me­ren met Kot­lin, deel 3

De nieu­we pro­gram­meer­taal voor Ja­va en meer, deel 3: func­ti­o­neel pro­gram­me­ren

C’t Magazine - - Inhoud - Chris­ti­an Helm­bold

Kot­lin is van be­gin af aan ont­wor­pen als een hy­bri­de pro­gram­meer­taal die zo­wel ob­ject­ge­o­ri­ën­teerd is, maar ook met func­ties werkt. Daar­mee komt hij ont­wik­ke­laars te­ge­moet, want func­ti­o­neel pro­gram­me­ren is in veel op­zich­ten een per­fec­te aan­vul­ling op een ob­ject­ge­o­ri­ën­teer­de pro­gram­meer­stijl.

Func­ti­o­neel pro­gram­me­ren werd al in 1958 ge­ïn­tro­du­ceerd met de pro­gram­meer­taal Lisp. In re­cen­te ja­ren is de in­te­res­se hie­rin flink toe­ge­no­men. Tech­ni­sche bal­last zo­als hulp­lijs­ten en for­loops zit­ten in ge­bruik­te func­ties ver­werkt. Ont­wik­ke­laars hoe­ven het wiel niet tel­kens weer op­nieuw uit te vin­den, hoe­ven min­der te tes­ten en je kunt ook geen fou­ten bij de im­ple­men­ta­tie ma­ken. En last but not least is func­ti­o­ne­le co­de vaak ook be­ter lees­baar dan im­pe­ra­tie­ve co­de.

Als je bij­voor­beeld uit een lijst met even en on­e­ven ge­tal­len de even ge­tal­len wilt fil­te­ren, dan moet je bij im­pe­ra­tie­ve pro­gram­meer­ta­len eerst een le­ge re­sul­ta­ten­lijst ma­ken, dan met een for-lus de bron­lijst door­lo­pen, ver­vol­gens elk ele­ment dat daar in zit met if con­tro­le­ren en al­le even­tu­e­le re­sul­ta­ten in de re­sul­ta­ten­lijst plaat­sen:

val list = lis­tOf(1, 2, 3, 5, 7) // im­pe­ra­ti­ve val re­sult = Ar­rayList<Int>() for (num­ber in list){ if (num­ber % 2 == 0)

re­sult.add(num­ber)

}

Met een func­ti­o­ne­le pro­gram­meer­taal kun je dit be­dui­dend kor­ter be­schrij­ven, waar­door de co­de ook stuk­ken lees­baar­der wordt:

val re­sult =

list.fil­ter { it % 2 == 0}

Van een ho­ge or­de

Een be­lang­rijk prin­ci­pe van func­ti­o­neel pro­gram­me­ren is dat func­ties en ge­ge­vens ge­lijk­waar­dig wor­den be­han­deld. Dat be­te­kent dat een func­tie aan een va­ri­a­be­le kan wor­den toe­ge­we­zen, hij aan een an­de­re func­tie als pa­ra­me­ter kan wor­den toe­ge­we­zen of door de an­de­re func­tie als re­sul­taat kan wor­den te­rug­ge­ge­ven. Dit is wat in de twee­de lis­ting ge­beurt.

De ex­pres­sie tus­sen de ac­co­la­des is een ano­nie­me func­tie. Der­ge­lij­ke func­ties wor­den vaak ook lamb­da-ex­pres­sies ge­noemd. Heel al­ge­meen ge­zegd geldt voor Kot­lin dat elk uit­voer­baar stuk­je co­de dat tus­sen ac­co­la­des staat, een ano­nie­me func­tie is:

val greet: () -> Unit =

{ print­ln("Hal­lo")}

De­ze ano­nie­me func­tie ac­cep­teert geen pa­ra­me­ters en le­vert geen waar­den op, hij voert en­kel iets uit. De func­tie wordt toe­ge­we­zen aan greet, een va­ri­a­be­le van het ty­pe () -> Unit. De le­ge haak­jes re­pre­sen­te­ren hier­bij de le­ge lijst pa­ra­me­ters en Unit re­pre­sen­teert het re­turn­ty­pe, wat in dit ge­val zo­iets be­te­kent als 'geen in­for­ma­tie'. Met Unit wordt als het wa­re het uit­zon­de­rings­ge­val void dat we ken­nen uit Ja­va een stan­daar­don­der­deel van het ty­pe­sys­teem, waar­door je het ook zon­der om­we­gen via ex­pli­cie­te in­ter­fa­ces kunt uit­druk­ken met nor­ma­le syn­tax.

In het vo­ri­ge voor­beeld zou je net zo mak­ke­lijk een ge­wo­ne func­tie kun­nen de­fi­ni­ë­ren. Het wordt pas echt nut­tig bij ano­nie­me func­ties, als je ze di­rect als pa­ra­me­ter door­geeft aan een an­de­re func­tie. Als je de bo­ven­staan­de func­tie bij­voor­beeld aan­vult met de pa­ra­me­ter na­me, dan kan hij aan de func­tie (fo­rEach) mee wor­den door­ge­ge­ven:

val na­mes =

lis­tOf("Mia", "Tim", "Ida") na­mes.fo­rEach

{ na­me -> print­ln("Hal­lo $na­me!")}

Daar­bij is na­me de naam van de eni­ge pa­ra­me­ter van de­ze func­tie. De lijst met pa­ra­me­ters is met een pijl van de romp ge­schei­den. Dat het ty­pe van de pa­ra­me­ter in dit voor­beeld een string is, snapt de com­pi­ler uit zich­zelf al. Maar je zou ook een dub­be­le punt ach­ter de pa­ra­me­ter kun­nen zet­ten om het lees­baar­der te ma­ken ( na­me: String -> … }). In­dien een ano­nie­me func­tie maar één pa­ra­me­ter heeft, dan kun je de lijst pa­ra­me­ters ach­ter­we­ge la­ten en met it naar de pa­ra­me­ter ver­wij­zen:

na­mes.fo­rEach

{ print­ln("Hal­lo $it!")}

Het con­cept dat een func­tie als pa­ra­me­ter aan een an­de­re func­tie wordt door­ge­ge­ven, wordt dui­de­lijk wan­neer je de in dit ge­val op­ti­o­ne­le haak­jes van de lijst pa­ra­me­ters van de fo­rEach-func­tie ge­bruikt:

na­mes.fo­rEach({ na­me ->

print­ln("Hal­lo $na­me!") })

Wan­neer de func­tie voor de uit­gif­te van de na­men geen ano­nie­me maar een ge­wo­ne func­tie was ge­weest, zou ze met een func­tie­ver­wij­zing aan fo­rEach door­ge­ge­ven kun­nen wor­den:

fun greet(na­me: String){

print­ln("Hal­lo $na­me!") } na­mes.fo­rEach(::greet)

Func­tie­ver­wij­zin­gen schrijf je met twee dub­be­le­pun­ten voor de func­tie­naam. Dat werkt ech­ter nog niet met de ac­tu­e­le Kot­lin ver­sie 1.1.51, maar is pas het ge­val van­af ver­sie 1.2. De laat­ste ex­pres­sie bin­nen een ano­nie­me func­tie is zijn re­turn­waar­de – daar is een ex­pli­cie­te re­turn ove­ri­gens niet voor no­dig. De vol­gen­de func­tie krijgt twee pa­ra­me­ters van het ty­pe Int, telt ze op en le­vert het re­sul­taat zon­der re­turn:

val calc =

{ a: Int, b: Int -> a + b}

Func­ties die an­de­re func­ties als pa­ra­me­ter krij­gen of een func­tie als uit­voer le­ve­ren, wor­den ho­ge­re-or­de­func­ties ge­noemd. De­ze spe­len een sleu­tel­rol om pa­ra­me­ters toe te wij­zen aan al­ge­me­ne func­ties en ze van lo­gi­ca voor con­cre­te si­tu­a­ties te voor­zien en zo her­bruik­baar te ma­ken.

Func­ties ter plaat­se

Ano­nie­me func­ties heb­ben het na­deel dat er op de ach­ter­grond nieu­we func­tie­ob­jec­ten wor­den ge­ge­ne­reerd die ex­tra ge­heu­gen in be­slag ne­men en waar ad­di­ti­o­ne­le me­tho­den voor moe­ten wor­den aan­ge­roe­pen. Ook moet de gar­ba­ge-col­lec­tor door de nieuw ge­ge­ne­reer­de ob­jec­ten meer werk ver­rich­ten. Bij het ont­wik­ke­len voor An­droid is het re­le­vant dat ook het to­ta­le aan­tal func­ties kan groei­en. Dit pro­bleem kun je in som­mi­ge ge­val­len ver­mij­den door een func­tie met in­li­ne te mar­ke­ren. De mo­di­fier in­li­ne geeft de com­pi­ler de in­struc­tie om de in­houd van een func­tie in

te bed­den op het mo­ment dat de­ze wordt aan­ge­roe­pen. De func­tie ti­me in het vol­gen­de voor­beeld be­staat dan met­een ook al niet meer tij­dens de run­ti­me, al­leen zijn in­houd:

im­port ja­va.lang.Sy­s­tem.:

.cur­rentTi­meMil­lis im­port ja­va.net.URL in­li­ne fun ti­me(f: () -> Unit){

.val start = cur­rentTi­meMil­lis()

.f()

.print­ln(cur­rentTi­meMil­lis() - start)

}

Uit de vol­gen­de oproep van ti­me

ti­me { URL("htt­ps://ct.nl")

.openCon­nec­ti­on()

}

maakt de com­pi­ler lo­gi­scher­wij­ze:

val start = cur­rentTi­meMil­lis() URL("htt­ps://ct.nl")

.openCon­nec­ti­on() print­ln(cur­rentTi­meMil­lis() - start)

Als in­li­ne al­leen maar voor­de­len zou heb­ben ge­had, zou de com­pi­ler de­ze me­tho­diek al­tijd kun­nen toe­pas­sen. Zo sim­pel ligt het ech­ter niet. Gro­te func­ties of func­ties die heel vaak wor­den aan­ge­roe­pen, kun­nen flink veel ge­heu­gen gaan be­slaan. Bo­ven­dien zijn er en­ke­le tech­ni­sche be­per­kin­gen om­dat in­li­ne-func­ties die pu­blic of pro­tec­ted zijn, de in­ter­ne of pri­va­te-ele­men­ten van hun mo­du­les niet mo­gen be­na­de­ren. Bo­ven­dien mo­gen ze geen in­ter­ne func­ties her­ber­gen noch re­cur­sief zijn.

Een an­de­re toe­pas­sing van in­li­ne­func­ties is om ty­pe­pa­ra­me­ters tij­dens run­ti­me be­schik­baar te stel­len. Nor­ma­li­ter zijn die tij­dens run­ti­me van de JVM niet be­schik­baar, om­dat in de­ze si­tu­a­tie ge­ne­rie­ke pro­gram­me­ring door het ver­wij­de­ren van de ty­pe-in­for­ma­tie (ty­pe era­su­re) om­ge­zet wordt. Aan­ge­zien in­li­ne-func­ties ech­ter wor­den in­ge­bed op de plek waar ze aan­ge­roe­pen wor­den, blijft het con­cre­te ty­pe in stand. Daar­voor plaats je voor een ty­pe­pa­ra­me­ter de op­dracht rei­fied (let­ter­lij­ke be­te­ke­nis: iets dat ab­stract is con­creet ma­ken):

in­li­ne fun <rei­fied T> showTy­pe() {

print­ln(T::class)

}

Het ty­pe van T is tij­dens de run­ti­me be­schik­baar in de func­tie showTy­pe, en wanDat neer showTy­pe<Dou­ble> () wordt aan­ge­roe­pen, krijg je class kot­lin.Dou­ble als re­sul­taat.

Ge­slo­ten ge­meen­schap

Een clo­su­re is een func­tie die de con­text kan be­na­de­ren van het mo­ment waar­op de func­tie werd ge­maakt. Va­ri­a­be­len uit de­ze con­text mo­gen door de clo­su­re ge­wij­zigd wor­den (re­al clo­su­re) – in te­gen­stel­ling tot in Ja­va, waar ze (qua­si) fi­nal moe­ten zijn. In het vol­gen­de voor­beeld wordt de va­ri­a­be­le coun­ter in de clo­su­re ge­wij­zigd:

var coun­ter =0 val in­cre­ment ={ coun­ter += 1} in­cre­ment() in­cre­ment() print­ln(coun­ter) // geeft 2

Zo­dra een clo­su­re ver­an­der­lij­ke va­ri­a­be­len in­te­greert, is er niet meer lan­ger spra­ke van een pu­re func­tie, want pu­re func­ties le­ve­ren bij de­zelf­de set pa­ra­me­ters al­tijd het­zelf­de re­sul­taat en heb­ben geen bij­wer­kin­gen.

Nieu­we col­lec­tie, func­ti­o­neel doch ele­gant

In de prak­tijk zal func­ti­o­neel pro­gram­me­ren in Kot­lin veel­al wor­den uit­ge­voerd met klas­sen en in­ter­fa­ces uit het kot­lin.col­lec­ti­ons-pak­ket, de stan­daard­bi­bli­o­theek. Die be­vat ty­pes zo­als Iter­able, List, Set en Map. Ver­an­der­lij­ke en on­ver­an­der­lij­ke col­lec­ti­ons zijn dui­de­lijk ge­schei­den. De in­ter­fa­ces voor on­ver­an­der­lij­ke col­lec­ti­ons he­ten Set, Map et ce­te­ra. Bij hun ver­an­der­lij­ke te­gen­han­gers staat er 'mu­ta­ble' voor, dus bij­voor­beeld Mu­ta­bleSet en Mu­ta­bleMap. In­stan­ces wor­den stan­daard ge­ge­ne­reerd vol­gens het pa­troon mu­ta­bleSetOf() , res­pec­tie­ve­lijk set­Of().

De func­ties voor het ver­wer­ken van col­lec­ti­ons zijn voor het groot­ste ge­deel­te om­ge­zet naar uit­brei­din­gen voor de Iter­able-in­ter­fa­ce. Een zo'n func­tie is bij­voor­beeld all met de vol­gen­de struc­tuur:

fun <T> Iter­able<T>.all(

pre­di­ca­te:(T) -> Boolean ): Boolean het hier om een uit­brei­dings­func­tie gaat, zie je aan dat het uit­breid­ba­re ty­pe Iter­able<T> voor de func­tie­naam all staat. Op de­ze ma­nier kun je vast­staan­de in­ter­fa­ces zo­als Iter­able ook met ei­gen func­ties uit­brei­den.

Daar­bij ge­bruik je all op de vol­gen­de ma­nier:

val s = set­Of(2, 5, 8, 10) val al­lE­ven = s.all { it % 2 == 0}

In dit ge­val krijgt de all-func­tie een ano­nie­me func­tie mee ({ it % 2 == 0 }), die test of al­le ele­men­ten even ge­tal­len zijn. De waar­de al­lE­ven is dus fal­se.

De te­gen­han­ger van all is de func­tie any. De­ze func­tie con­tro­leert of er über­haupt een ele­ment is dat aan de voor­waar­de vol­doet:

val anyE­ven = s.any { it % 2 == 0}

De fil­ter-func­tie wordt bij­zon­der vaak ge­bruikt. De­ze ver­za­melt al­le ele­men­ten die aan de voor­waar­den vol­doen:

val evenNum­bers: List<Int>=

s.fil­ter { it % 2 == 0}

In dit voor­beeld wor­den al­le even ge­tal­len ver­za­meld en in een nieu­we lijst ge­plaatst. Dat elk van de­ze func­ties een nieu­we col­lec­ti­on ge­ne­reert en niet de be­staan­de col­lec­ti­on wij­zigt, is een ba­sis­prin­ci­pe van het func­ti­o­neel pro­gram­me­ren.

Net zo ge­brui­ke­lijk is de map-func­tie, die in­voer­waar­den via een trans­for­ma­tie­func­tie weer­geeft naar uit­voer­waar­den. Al­le waar­den van on­ze klei­ne voor­beeldset wor­den hier ver­dub­beld:

val n = s.map { it * 2}

Plat­land

De func­tie is­fla­tMap is een va­ri­ant van de map-func­tie waar­mee je in­ge­bed­de ge­ge­vens­struc­tu­ren plat maakt, waar­bij dus al­le ele­men­ten aan een lijst wor­den toe­ge­we­zen. Een blik op de stan­daard toe­pas­sing van fla­tMap maakt het iets dui­de­lij­ker:

in­li­ne fun <T, R> Iter­able<T>.fla­tMap(

trans­form:(T) -> Iter­able<R> ): List<R>

De ma­nier waar­op fla­tMap werkt, is het mak­ke­lijkst aan te to­nen door het di­rect te ver­ge­lij­ken met map: De lis­ting

val l = lis­tOf(1, 2, 3)

val re­sult = l.map { lis­tOf(it,-it)} print­ln(re­sult)

ge­ne­reert een lijst van lijs­ten: [[1, -1], [2, -2], [3, -3]]. Met de func­tie flat­ten maak je de­ze in­ge­bed­de struc­tuur ten slot­te plat: l.flat­ten() geeft [1, -1, 2, -2, 3, -3] als re­sul­taat.

Het­zelf­de re­sul­taat krijg je in één keer met fla­tMap. Dat ver­loopt zo­als hier­bo­ven, maar ver­vol­gens wor­den de waar­den uit de bin­nen­ste lijst naar de re­sul­ta­ten ge­schre­ven – de struc­tuur wordt plat. De co­de

val re­sult = l.fla­tMap { lis­tOf(it, -it) } print­ln(re­sult)

geeft het re­sul­taat

[1, -1, 2, -2, 3, -3]

Sleu­tel­ma­ker

De as­so­ci­a­te-func­tie trans­for­meert een Iter­able ele­ment voor ele­ment naar sleu­tel/waar­de-pa­ren. Hier ge­ne­reert de func­tie uit­ein­de­lijk een map uit. Een voor­beeld hier­van is een lijst van strings met na­men en te­le­foon­num­mers, waar je een map van maakt:

val pho­neList = lis­tOf("Ingrid:2915", "Karel:3892") val pho­neMap = pho­neList

.map{ it.split(":")}

.as­so­ci­a­te {(na­me, num­ber) ->

Pair(na­me, num­ber)} print­ln(pho­neMap["Karel”]) // "3892" Al­ler­eerst moe­ten de strings op de po­si­tie van de dub­be­le punt in twee­ën ge­deeld wor­den om de naam en het te­le­foon­num­mer te split­sen. Dat zou je in fei­te ook door de ano­nie­me func­tie kun­nen la­ten doen die aan as­so­ci­a­te wordt ge­kop­peld. Maar dat zou niet stro­ken met het prin­ci­pe dat je met een­vou­di­ge, on­af­han­ke­lij­ke func­ties werkt. Daar­om wor­den de strings in een ei­gen stap met be­hulp van de map-func­tie ge­schre­ven naar lijs­ten met elk twee ele­men­ten, na­me­lijk naam en te­le­foon­num­mer.

Pas in de vol­gen­de stap wor­den daar Pair-in­stan­ties van ge­maakt waar as­so­ci­a­te dan een map uit ge­ne­reert. De pa­ra­me­ter die aan as­so­ci­a­te is ge­kop­peld, wordt een de­struc­tu­ring-de­cla­ra­ti­on ge­noemd. De­ze func­tie deelt de ont­van­gen waar­den na­me­lijk met­een op in de on­der­de­len na­me en num­ber.

Om het over­zich­te­lij­ker te hou­den, is het veel­al han­dig om aan el­kaar ge­kop­pel­de func­tie­oproe­pen over af­zon­der­lij­ke com­po­nen­ten te ver­de­len en hun tus­sen­re­sul­ta­ten te be­noe­men. Het wordt nog dui­de­lij­ker wan­neer je ty­pes ex­pli­ciet be­noemt. Het voor­beeld hier­bo­ven komt er dan als volgt uit te zien:

val pho­neList: List<String>=

lis­tOf("Ingrid:2915", "Karel:3892") val na­mesNum­bers: List<List<String>> =

pho­neList.map{ it.split(":")} val pho­neMap: Map<String, String>= na­mesNum­bers.as­so­ci­a­te {

(na­me, num­ber) ->

Pair(na­me, num­ber)}

In het vol­gen­de voor­beeld gaan we met groupBy tien na­men groe­pe­ren op ba­sis van de leng­te (aan­tal te­kens). Het re­sul­taat is een map waar­van de ele­men­ten met de on­der­staan­de map-func­tie via Pair wor­den op­ge­bouwd:

val na­mes = lis­tOf("Ben", "Paul", "Jo­nas","Eli­as","Le­on","Finn", "No­ah","Luis","Lu­k­as","Felix") val fre­quen­cies = na­mes

.groupBy{ it.length }

.map {(length, na­mes) ->

Pair(length, na­mes.si­ze)}

Als je de func­tie­aan­roe­pen in los­se de­len op­splitst, dan kun je de tus­sen­re­sul­ta­ten la­ten weer­ge­ven:

val fre­qs = na­mes

.groupBy{ it.length } print­ln(fre­qs) val fre­quen­cies = fre­qs

.map {(length, na­mes) ->

Pair(length, na­mes.si­ze)} print­ln(fre­quen­cies)

De eer­ste groep uit­ge­voer­de func­ties le­vert dan {3=[Ben], 4=[Paul, Le­on, Finn, No­ah, Luis], 5=[Jo­nas, Eli­as, Lu­k­as, Felix]} op. De twee­de set re­sul­ta­ten is [(3, 1), (4, 5), (5, 4)]. Er is dus één naam van drie te­kens, vijf na­men van vier te­kens en vier na­men met vijf te­kens.

De fold-func­tie ver­za­melt waar­den. Daar­voor geeft fold de ini­ti­ë­le waar­de en het eer­ste ele­ment van de col­lec­tie aan een func­tie door, die daar een waar­de uit ge­ne­reert.

Het re­sul­taat is we­der­om de ini­ti­ë­le waar­de voor de vol­gen­de oproep met het vol­gen­de ele­ment. Als je de ge­tal­len 1, 2, 3, 4, 5 met fold wilt op­som­men, kies je de 0 als star­tele­ment, zo­dat het ver­loop (((((0 + 1) + 2) + 3) + 4) + 5) wordt. De haak­jes zijn hier­bij al­leen ter ver­dui­de­lij­king.

Die co­de ziet er zo uit:

lis­tOf(1, 2, 3, 4, 5)

.fold(0){ a, b -> a + b}

Ten slot­te la­ten we hier nog even de func­tie zip zien. De­ze com­bi­neert af­wis­se­lend ele­men­ten uit twee iter­ables – net als een rits­slui­ting:

val a = lis­tOf(1, 2, 3, 4) val b = lis­tOf(9, 8, 7, 6) val ab = a.zip(b)

heeft als re­sul­taat:

[(1, 9), (2, 8), (3, 7), (4, 6)]

Se­quen­ties

Op­per­vlak­kig ge­zien ziet de co­de voor het ver­wer­ken van Se­quen­ce en Iter­able er bij­na iden­tiek uit. Het ver­schil zit hem in de werk­wij­ze: de func­ties in iter­able ver­wer­ken al­le ele­men­ten in de col­lec­ti­on en ge­ne­re­ren dan een nieu­we nieu­we col­lec­ti­on die de in­voer voor de vol­gen­de func­tie vormt, en ga zo maar door. Bij se­quen­ties door­lo­pen al­le ele­men­ten van de oor­spron­ke­lij­ke col­lec­ti­on een voor een de ge­he­le func­tie­ke­ten.

Dat is han­dig als je met een the­o­re­tisch on­be­grens­de reeks van ele­men­ten aan het werk bent, zo­als bij de Fi­bo­n­ac­ci-reeks, of wan­neer je met een gro­te hoe­veel­heid ge­ge­vens werkt. Een voor­beeld is het ver­wer­ken van ge­ge­vens die niet eerst vol­le­dig in het werk­ge­heu­gen ge­la­den wor­den, maar stap voor stap ver­werkt moe­ten wor­den.

Om een groot be­stand in stap­pen te ver­wer­ken, is er de func­tie useLi­nes uit de stan­daard­bi­bli­o­theek. De­ze func­tie biedt het be­stand dan aan als een se­quen­tie van re­gels. Wij heb­ben als test een lijst met zo'n 20.000 voor­na­men re­gel voor re­gel la­ten door­zoe­ken, en daar­bij ge­zocht naar de kort­ste voor­naam:

val shor­test = Fi­le("Voor­na­men.txt") .useLi­nes(Char­sets.ISO_8859_1){ li­nes ->

li­nes.minBy { it.length }

} print­ln(shor­test)

… wat in dit ge­val de op­val­lend kor­te naam 'O' op­le­ver­de.

Een na de an­der

Se­quen­ties zijn ook han­dig in­dien het niet no­dig is om al­le ele­men­ten te ver­wer­ken om tot een re­sul­taat te ko­men. Voor de func­tie any wor­den de ele­men­ten net zo­lang ver­werkt tot­dat er één aan de eer­ste con­di­tie vol­doet:

list.any { it < 0}

Met de func­tie ge­ne­ra­teSe­quen­ce kun je zelf een se­quen­tie ma­ken:

fun fi­bo­n­ac­ci(): Se­quen­ce<Int>{ re­turn ge­ne­ra­teSe­quen­ce( Pair(0, 1), { Pair(it.se­cond,

it.first + it.se­cond)} ).map { it.first } } val re­sult = fi­bo­n­ac­ci().ta­ke(10)

.joinToString(", ") print­ln(re­sult)

Met als re­sul­taat:

0, 1, 1, 2, 3, 5, 8, 13, 21, 34

Wan­neer je van een se­quen­tie een stan­daard col­lec­ti­on wilt ge­ne­re­ren, moet er een zo­ge­naam­de 'ter­mi­nal ope­ra­ti­on' wor­den uit­ge­voerd, zo­als bij toList. De­ze func­tie be­ëin­digt de se­quen­ti­ë­le ver­wer­king en 'raapt' de ele­men­ten bij el­kaar. De di­rec­te be­oor­de­ling zo­als bij Iter­able wordt ook wel 'ea­ger' ge­noemd, de be­oor­de­ling die al­leen door­gaat zo lang als no­dig, zo­als bij Se­quen­ce, heet 'la­zy'

Col­lec­ti­ons zo­als List, Set et ce­te­ra, zou je met de func­tie asSe­quen­ce naar een se­quen­tie kun­nen om­zet­ten:

myList.asSe­quen­ce() .fil­ter { it % 2 == 0} .toList()

Se­quen­ties in Kot­lin ko­men in prin­ci­pe over­een met de streams van Ja­va 8. Om­dat je bij een col­lec­ti­on niet eerst stream()en la­ter ook nog iets als col­lect(Col­lec­tors. toList())hoeft uit te voe­ren, zijn col­lec­ti- ons iets ge­bruiks­vrien­de­lij­ker. Een na­deel is ech­ter dat se­quen­ties in Kot­lin van huis uit niet pa­ral­lel uit­ge­voerd kun­nen wor­den. In de (re­de­lijk on­ge­brui­ke­lij­ke) ge­val­len dat een pa­ral­lel­le ver­wer­king han­dig is, kun je na­tuur­lijk al­tijd te­rug­grij­pen op de streams uit JDK.

Het ein­de van de re­cur­sie

De om­gang met func­ti­o­neel pro­gram­me­ren ver­eist eni­ge in­werk­tijd, maar kan in veel ge­val­len ele­gan­te­re co­de op­le­ve­ren. Rou­ti­nes die vaak te­rug­ke­ren, pak je het bes­te aan met re­cur­sie­ve func­ties, of­te­wel func­ties die zich­zelf op­roe­pen. Dit is het meest ge­schikt wan­neer er boom­struc­tu­ren door­zocht moe­ten wor­den, zo­als een di­rec­to­ry­s­truc­tuur:

fun walk(fi­le: Fi­le){ if (fi­le.isFi­le)

print­ln(fi­le) el­se if (fi­le.isDi­rec­to­ry) fi­le.lis­tFi­les().fo­rEach {

walk(it)

}

}

De com­pi­ler kan zeer ef­fi­ci­ën­te co­de voor re­cur­sie­ve func­ties ge­ne­re­ren wan­neer de re­cur­sie aan het ein­de van de func­tie plaats­vindt. Dit sce­na­rio wordt ook wel 'end re­cur­si­on' ge­noemd. Als een func­tie met het sleu­tel­woord tail­rec ge­mar­keerd is, wordt de­ze na­me­lijk naar een whi­le-loop ver­taald, die de pro­ces­sor snel­ler kan uit­voe­ren. Bo­ven­dien kan zo'n loop geen stack-over­flow ver­oor­za­ken, een ge­vaar dat bij een die­pe re­cur­sie op de loer ligt. Bij re­cur­sie­ve func­ties moet al­tijd het out­put­ty­pe aan­ge­ge­ven wor­den, om­dat de com­pi­ler een re­la­tief een­vou­dig al­go­rit­me als ty­pe­af­lei­ding ge­bruikt om snel­ler te kun­nen wer­ken.

fun fac­to­ri­al(num: Int): Long { tail­rec fun fac­to­ri­al(

acc: Long, num: Int): Long = if (num == 1)

acc el­se

fac­to­ri­al(num * acc, num - 1) re­turn fac­to­ri­al(0, num)

(ddu) c

Li­te­ra­tur

[1] Chris­ti­an Helm­bold, Ja­va is pas­sé, De nieu­we pro­gram­meer­taal voor Ja­va en meer, deel 1, c't 1-2/2018, p.138

[2] Chris­ti­an Helm­bold, Klas­se klas­sen, De nieu­we pro­gram­meer­taal voor Ja­va en meer, deel 1, c't 3/2018, p.132

Een goed over­zicht van de func­ties voor col­lec­ti­ons vind je in de API-do­cu­men­ta­tie.

Met In­tel­liJ IDEA kun je break­points ook plaat­sen in ano­nie­me func­ties.

Newspapers in Dutch

Newspapers from Netherlands

© PressReader. All rights reserved.