Hvordan jobbe med Elixir Comprehensions

Elixir er et veldig ung programmeringsspråk (oppstått i 2011), men det er stadig mer populært. Jeg var opprinnelig interessert i dette språket fordi når du bruker det, kan du se på noen vanlige oppgaver som programmerere vanligvis løser fra en annen vinkel. For eksempel kan du finne ut hvordan du kan iterere over samlinger uten til syklus, eller hvordan du organiserer koden din uten klasser.

Elixir har noen veldig interessante og kraftige funksjoner som kan være vanskelig å få hodet rundt om du kom fra OOP-verdenen. Men etter en tid begynner alt å være fornuftig, og du ser hvordan uttrykksfulle funksjonskoden kan være. Forståelse er en slik funksjon, og denne artikkelen vil jeg forklare hvordan jeg skal jobbe med dem.

Forståelse og kartlegging

Generelt sett er en listeforståelse en spesiell konstruksjon som lar deg lage en ny liste basert på eksisterende. Dette konseptet finnes på språk som Haskell og Clojure. Erlang presenterer også det, og derfor har Elixir også forståelser.

Du kan spørre hvordan forståelse er forskjellig fra kart / 2-funksjonen, som også tar en samling og produserer en ny? Det ville være et rettferdig spørsmål! Vel, i det enkleste tilfellet, gjør forståelser stort sett det samme. Ta en titt på dette eksemplet:

defmodule MyModule gjør def do_something (liste) gjøre liste |> Enum.map (fn (el) -> el * 2 ende) ende ende MyModule.do_something ([1,2,3])> IO.inspect # => [ 2,4,6]

Her tar jeg bare en liste med tre tall og produserer en ny liste med alle tallene multiplisert med 2. De kart Anrop kan videre forenkles som Enum.map (& (& 1 * 2)).

De do_something / 1 funksjonen kan nå omskrives med en forståelse:

 def do_something (liste) gjør for el <- list, do: el * 2 end

Dette er hva en grunnleggende forståelse ser ut, og etter min mening er koden litt mer elegant enn i det første eksemplet. Her tar vi igjen hvert element fra listen og multipliserer det med 2. De el <- list en del kalles a generator, og det forklarer hvordan akkurat du ønsker å trekke verdiene fra samlingen din.

Merk at vi ikke er tvunget til å sende en liste til do_something / 1 funksjon-koden vil fungere med alt som er tallrike:

defmodule MyModule gjør def do_something (samling) gjør for el <- collection, do: el * 2 end end MyModule.do_something((1… 3)) |> IO.inspect

I dette eksemplet passerer jeg et område som et argument.

Forståelser jobber med binstrings også. Syntaxen er litt annerledes da du trenger å legge inn generatoren din med << og >>. La oss demonstrere dette ved å lage en veldig enkel funksjon for å "dechifisere" en streng beskyttet med en Caesar-kryptering. Ideen er enkel: Vi erstatter hvert brev i ordet med et brev et fast antall stillinger ned i alfabetet. Jeg skal flytte forbi 1 posisjon for enkelhet:

defmodule MyModule gjør def dechiffrere (chiffer) gjør for << char <- cipher >>, gjør: char - 1 ende ende MyModule.decipher ("fmjyjs") |> IO.inspect # => 'elixir'

Dette ser stort sett ut som det forrige eksemplet med unntak av << og >> deler. Vi tar en kode for hvert tegn i en streng, reduserer den for en, og konstruerer en streng tilbake. Så den krypterte meldingen var "eliksir"!

Men fortsatt er det mer enn det. En annen nyttig funksjon av forståelser er evnen til å filtrere ut noen elementer.

Forståelse og filtrering

La oss videre utvide vårt første eksempel. Jeg skal passere en rekke heltal fra 1 til 20, ta bare elementene som er like, og formere dem med 2:

defmodule MyModule krever Integer def do_something (samling) gjør samling |> Stream.filter (& Integer.is_even / 1) |> Enum.map (& (& 1 * 2)) endeenden MyModule.do_something ((1 ... 20)) | > IO.inspect

Her måtte jeg kreve Integer modul for å kunne bruke is_even / 1 makro. Også, jeg bruker Strøm å optimalisere koden litt og forhindre at iterasjonen blir utført to ganger.

La oss nå skrive om dette eksemplet med en forståelse igjen:

 def do_something (samling) gjør for el <- collection, Integer.is_even(el), do: el * 2 end

Så, som du ser, til kan godta et valgfritt filter for å hoppe over noen elementer fra samlingen.

Du er ikke begrenset til bare ett filter, så følgende kode er også legitim:

 def do_something (samling) gjør for el <- collection, Integer.is_even(el), el < 10, do: el * 2 end

Det vil ta alle like tall mindre enn 10. Ikke glem å avgrense filtre med kommaer.

Filtrene vil bli evaluert for hvert element i samlingen, og hvis evalueringen returnerer ekte, blokken blir utført. Ellers blir et nytt element tatt. Det som er interessant er at generatorer også kan brukes til å filtrere ut elementer ved å bruke når:

 def do_something (samling) gjør for el når el < 10 <- collection, Integer.is_even(el), do: el * 2 end

Dette ligner veldig på hva vi gjør når du skriver vaktklausuler:

def do_something (x) når is_number (x) gjør # ... ende

Forståelser med flere samlinger

Anta nå at vi ikke har en, men to samlinger på en gang, og vi vil gjerne produsere en ny samling. Ta for eksempel alle like tall fra den første samlingen og merkelig fra den andre, og multipliser dem deretter:

defmodule MyModule krever Integer def do_something (collection1, collection2) gjør for el1 <- collection1, el2 <- collection2, Integer.is_even(el1), Integer.is_odd(el2), do: el1 * el2 end end MyModule.do_something( (1… 20), (5… 10) ) |> IO.inspect

Dette eksemplet illustrerer at forståelser kan fungere med mer enn én samling på en gang. Det første like tallet fra collection1 vil bli tatt og multiplisert med hvert oddetall fra collection2. Deretter er det andre enda heltallet fra collection1 vil bli tatt og multiplisert, og så videre. Resultatet vil være: 

[10, 14, 18, 20, 28, 36, 30, 42, 54, 40, 56, 72, 50, 70, 90, 60, 84, 108, 70, 98, 126, 80, 112, 144, 90 , 126, 162, 100, 140, 180]

Dessuten er de resulterende verdiene ikke nødvendige for å være heltall. For eksempel kan du returnere en tuple som inneholder heltall fra første og andre samlinger:

defmodule MyModule krever Integer def do_something (collection1, collection2) gjør for el1 <- collection1, el2 <- collection2, Integer.is_even(el1), Integer.is_odd(el2), do: el1,el2 end end MyModule.do_something( (1… 20), (5… 10) ) |> IO.inspect # => [2, 5, 2, 7, 2, 9, 4, 5 ...]

Forståelse med "Into" -alternativet

Frem til dette punktet var det endelige resultatet av vår forståelse alltid en liste. Dette er faktisk ikke, heller ikke obligatorisk. Du kan spesifisere en inn i parameter som aksepterer en samling for å inneholde den resulterende verdien. 

Denne parameteren godtar hvilken som helst struktur som implementerer samleprotokollen, slik at vi for eksempel kan generere et kart slik:

defmodule MyModule krever Integer def do_something (collection1, collection2) gjør for el1 <- collection1, el2 <- collection2, Integer.is_even(el1), Integer.is_odd(el2), into: Map.new, do: el1,el2 end end MyModule.do_something( (1… 20), (5… 10) ) |> IO.inspect # =>% 2 => 9, 4 => 9, 6 => 9 ...

Her sa jeg bare inn i: Map.new, som også kan erstattes med inn i:% . Ved å returnere el1, el2 tuple, satte vi i utgangspunktet det første elementet som en nøkkel og den andre som verdien.

Dette eksempelet er ikke spesielt nyttig, men la oss da generere et kart med et tall som en nøkkel og dens firkant som en verdi:

defmodule MyModule gjør def do_something (samling) gjør for el <- collection, into: Map.new, do: el, :math.sqrt(el) end end squares = MyModule.do_something( (1… 20) ) |> IO.inspect # =>% 1 => 1.0, 2 => 1.4142135623730951, 3 => 1.7320508075688772, ... firkanter [3] |> IO.puts # => 1.7320508075688772

I dette eksemplet bruker jeg Erlangs :matte modul direkte, som for alle modulers navn er atomer. Nå kan du lett finne plassen for et hvilket som helst nummer fra 1 til 20.

Forståelse og mønstertilpasning

Den siste tingen å nevne er at du kan utføre mønstermatching i forståelser også. I noen tilfeller kan det komme ganske bra.

Anta at vi har et kart som inneholder ansattes navn og deres rå lønn:

% "Joe" => 50, "Bill" => 40, "Alice" => 45, "Jim" => 30

Jeg vil generere et nytt kart hvor navnene er nedskrevet og konvertert til atomer, og lønn beregnes ved hjelp av en skattesats:

defmodule MyModule gjør @tax 0.13 def format_employee_data (samling) gjør for navn, lønn <- collection, into: Map.new, do: format_name(name), salary - salary * @tax end defp format_name(name) do name |> String.downcase |> String.to_atom slutten MyModule.format_employee_data (% "Joe" => 50, "Bill" => 40, "Alice" => 45, "Jim" => 30)> IO.inspect # =>% alice: 39,15, regning: 34,8, jim: 26,1, joe: 43,5

I dette eksemplet definerer vi en modulattributt @avgift med et vilkårlig tall. Da dekonstruerer jeg dataene i forståelsen med navn, lønn <- collection. Til slutt formatere navnet og beregne lønnen etter behov, og lagre resultatet i det nye kartet. Ganske enkelt, men uttrykksfulle.

Konklusjon

I denne artikkelen har vi sett hvordan du bruker Elixir-forståelser. Du kan trenge litt tid til å bli vant til dem. Denne konstruksjonen er veldig pen og i noen situasjoner kan passe inn mye bedre enn funksjoner som kart og filter. Du finner flere eksempler i Elixirs offisielle doks og startveiledningen.

Forhåpentligvis har du funnet denne opplæringen nyttig og interessant! Takk for at du bodde hos meg og ser deg snart.