Hur man arbetar med Elixir Comprehensions

Elixir är ett mycket ungt programmeringsspråk (framkom 2011), men det blir populärare. Jag var initialt intresserad av detta språk eftersom när du använder den kan du titta på några vanliga uppgifter som programmerare brukar lösa från en annan vinkel. Till exempel kan du ta reda på hur man kan iterera över samlingar utan för cykla eller hur du organiserar din kod utan klasser.

Elixir har några mycket intressanta och kraftfulla funktioner som kan vara svåra att ta sig runt om du kom från OOP-världen. Men efter en tid börjar allt att vara meningsfullt, och du ser hur uttrycksfull funktionskoden kan vara. Förståelser är en sådan funktion, och den här artikeln kommer jag att förklara hur man arbetar med dem.

Förståelser och kartläggning

I allmänhet är en listaförståelse en speciell konstruktion som gör att du kan skapa en ny lista baserad på befintliga. Detta begrepp finns på språk som Haskell och Clojure. Erlang presenterar också det och därför har Elixir också förståelse.

Du kanske frågar hur skillnader skiljer sig från kartan / 2-funktionen, som också tar en samling och producerar en ny? Det skulle vara en rättvis fråga! Tja, i det enklaste fallet gör förståelser ganska mycket samma sak. Ta en titt på detta exempel:

defmodule MyModule gör def do_something (lista) lista |> Enum.map (fn (el) -> el * 2 ände) änden MyModule.do_something ([1,2,3])> IO.inspect # => [ 2,4,6]

Här tar jag bara en lista med tre siffror och producerar en ny lista med alla siffror multiplicerat med 2. De Karta samtalet kan förenklas ytterligare som Enum.map (& (& 1 * 2)).

De do_something / 1 funktionen kan nu omskrivas med hjälp av en förståelse:

 def do_something (lista) gör för el <- list, do: el * 2 end

Så här ser en grundläggande förståelse ut, och enligt min mening är koden lite mer elegant än i det första exemplet. Här tar vi än en gång varje element från listan och multiplicerar det med 2. De el <- list en del kallas a generator, och det förklarar hur exakt du vill extrahera värdena från din samling.

Observera att vi inte är tvungna att skicka en lista till do_something / 1 funktion-koden kommer att fungera med allt som är uppräkningsbart:

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

I det här exemplet passerar jag ett intervall som ett argument.

Förståelser fungerar också med binstrings. Syntaxen är lite annorlunda eftersom du måste bifoga din generator med << och >>. Låt oss demonstrera detta genom att skapa en mycket enkel funktion att "dechiffrera" en sträng som är skyddad med en Caesar-chiffer. Tanken är enkel: Vi ersätter varje bokstav i ordet med ett brev ett fast antal positioner ner i alfabetet. Jag kommer att flytta om 1 läge för enkelhet:

defmodule MyModule gör def dechiffrera (chiffer) göra för << char <- cipher >>, gör: char - 1 änden MyModule.decipher ("fmjyjs") |> IO.inspect # => 'elixir'

Detta ser ungefär ut som det föregående exemplet med undantag för << och >> delar. Vi tar en kod för varje tecken i en sträng, minskar den med en och bygger en sträng tillbaka. Så det chifferade meddelandet var "elixir"!

Men det finns fortfarande mer än så. Ett annat användbart inslag i förståelse är möjligheten att filtrera bort vissa element.

Förståelse och filtrering

Låt oss ytterligare utöka vårt första exempel. Jag kommer att skicka en rad heltal från 1 till 20, ta bara de element som är jämn och multiplicera dem med 2:

defmodule MyModule kräver Integer def do_something (samling) gör samling |> Stream.filter (& Integer.is_even / 1) |> Enum.map (& (& 1 * 2)) slutänden MyModule.do_something ((1 ... 20)) | > IO.inspect

Här var jag tvungen att kräva Heltal modul för att kunna använda is_even / 1 makro. Jag använder också Strömma att optimera koden lite och förhindra att iterationen utförs två gånger.

Låt oss nu skriva om detta exempel med en förståelse igen:

 def do_something (samling) gör för el <- collection, Integer.is_even(el), do: el * 2 end

Så som du ser, för kan acceptera ett valfritt filter för att hoppa över några element från samlingen.

Du är inte begränsad till bara ett filter, så följande kod är också legitim:

 def do_something (samling) gör för el <- collection, Integer.is_even(el), el < 10, do: el * 2 end

Det kommer att ta alla jämntal mindre än 10. Glöm inte att avgränsa filter med kommatecken.

Filtret kommer att utvärderas för varje element i samlingen, och om utvärderingen returnerar Sann, blocket exekveras. Annars tas ett nytt element. Det som är intressant är att generatorer också kan användas för att filtrera ut element genom att använda när:

 def do_something (samling) gör för el när el < 10 <- collection, Integer.is_even(el), do: el * 2 end

Det här ligner mycket på vad vi gör när vi skriver vaktklausuler:

def do_something (x) när is_number (x) gör # ... slutet

Förståelser med flera samlingar

Antag nu att vi inte har en utan två samlingar på en gång, och vi skulle vilja producera en ny samling. Ta till exempel alla jämntal från den första samlingen och udda från den andra och multiplicera dem sedan:

defmodule MyModule kräver integer def do_something (collection1, collection2) gör för 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

Detta exempel illustrerar att förståelser kan fungera med mer än en samling på en gång. Det första jämntalet från collection1 kommer att tas och multipliceras med varje udda nummer från collection2. Nästa, det andra jämntalet från collection1 kommer att tas och multipliceras, och så vidare. Resultatet blir: 

[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]

Dessutom behöver de resulterande värdena inte vara heltal. Till exempel kan du returnera en tupel som innehåller heltal från den första och den andra samlingen:

defmodule MyModule kräver integer def do_something (collection1, collection2) gör för 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 ...]

Förståelser med "in" -alternativet

Fram till den här tiden var det slutliga resultatet av vår förståelse alltid en lista. Detta är i själva verket inte heller obligatoriskt. Du kan ange en in i parameter som accepterar en samling för att innehålla det resulterande värdet. 

Den här parametern accepterar vilken struktur som implementerar protokollet Collectible, så vi kan till exempel generera en karta så här:

defmodule MyModule kräver integer def do_something (collection1, collection2) gör för 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 ...

Här sa jag helt enkelt in i: Map.new, som också kan ersättas med in i: %. Genom att returnera el1, el2 tuple, ställer vi i grunden det första elementet som en nyckel och den andra som värdet.

Detta exempel är inte särskilt användbart, men låt oss generera en karta med ett tal som en nyckel och dess kvadrat som ett värde:

defmodule MyModule gör def do_something (samling) gör för 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, ... kvadrater [3] |> IO.puts # => 1.7320508075688772

I det här exemplet använder jag Erlangs :matematik modul direkt, som trots allt alla modulers namn är atomer. Nu kan du enkelt hitta torget för valfritt nummer från 1 till 20.

Förståelser och mönster matchning

Det sista att nämna är att du kan utföra mönstermatchning i förståelser också. I vissa fall kan det komma ganska praktiskt.

Antag att vi har en karta med anställda namn och deras råa löner:

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

Jag vill skapa en ny karta där namnen är nedskurna och omvandlas till atomer, och löner beräknas med en skattesats:

defmodule MyModule gör @tax 0.13 def format_employee_data (samling) gör för namn, lön <- collection, into: Map.new, do: format_name(name), salary - salary * @tax end defp format_name(name) do name |> String.downcase |> String.to_atom slutändan MyModule.format_employee_data (% "Joe" => 50, "Bill" => 40, "Alice" => 45, "Jim" => 30)> IO.inspect # =>% alice: 39,15, bill: 34,8, jim: 26,1, Joe: 43,5

I det här exemplet definierar vi en modulattribut @beskatta med ett godtyckligt nummer. Sedan dekonstruerar jag data i förståelsen med namn, lön <- collection. Slutligen formatera namnet och beräkna lön efter behov och lagra resultatet i den nya kartan. Ganska enkelt men uttrycksfullt.

Slutsats

I denna artikel har vi sett hur man använder Elixir-förståelser. Du kan behöva lite tid att vänja sig på dem. Denna konstruktion är riktigt snygg och i vissa situationer kan passa in mycket bättre än funktioner som Karta och filtrera. Du kan hitta några fler exempel i Elixirs officiella dokument och startguiden.

Förhoppningsvis har du hittat denna handledning användbar och intressant! Tack för att du bodde hos mig och vi ses snart.