Introduktion til Clojure

1. Introduktion

Clojure er et funktionelt programmeringssprog, der kører udelukkende på Java Virtual Machine, på samme måde som Scala og Kotlin. Clojure betragtes som et Lisp-derivat og vil være kendt for alle, der har erfaring med andre Lisp-sprog.

Denne vejledning giver en introduktion til Clojure-sproget, der introducerer, hvordan man kommer i gang med det, og nogle af nøglebegreberne for, hvordan det fungerer.

2. Installation af Clojure

Clojure er tilgængelig som installatører og praktisk scripts til brug på Linux og macOS. Desværre har Windows ikke et sådant installationsprogram på dette tidspunkt.

Linux-scripts fungerer dog muligvis i noget som Cygwin eller Windows Bash. Der er også en onlinetjeneste, der kan bruges til at teste sproget, og ældre versioner har en enkeltstående version, der kan bruges.

2.1. Uafhængig download

Den uafhængige JAR-fil kan downloades fra Maven Central. Desværre fungerer versioner, der er nyere end 1.8.0, ikke længere let på grund af, at JAR-filen er opdelt i mindre moduler.

Når denne JAR-fil er downloadet, kan vi bruge den som en interaktiv REPL ved blot at behandle den som en eksekverbar JAR:

$ java -jar clojure-1.8.0.jar Clojure 1.8.0 bruger =>

2.2. Webinterface til REPL

En webgrænseflade til Clojure REPL er tilgængelig på //repl.it/languages/clojure, så vi kan prøve uden at skulle downloade noget. I øjeblikket understøtter dette kun Clojure 1.8.0 og ikke de nyere udgivelser.

2.3. Installatør på MacOS

Hvis du bruger macOS og har Homebrew installeret, kan den seneste version af Clojure let installeres:

$ bryg installering clojure

Dette understøtter den nyeste version af Clojure - 1.10.0 i skrivende stund. Når vi er installeret, kan vi indlæse REPL ved blot at bruge clojure eller clj kommandoer:

$ clj Clojure 1.10.0 bruger =>

2.4. Installatør på Linux

Et selvinstallerende shell-script er tilgængeligt for os til at installere værktøjerne på Linux:

$ curl -O //download.clojure.org/install/linux-install-1.10.0.411.sh $ chmod + x linux-install-1.10.0.411.sh $ sudo ./linux-install-1.10.0.411.sh

Som med macOS-installationsprogrammet vil disse være tilgængelige for de seneste udgivelser af Clojure og kan udføres ved hjælp af clojure eller clj kommandoer.

3. Introduktion til Clojure REPL

Alle ovenstående muligheder giver os adgang til Clojure REPL. Dette er den direkte Clojure-ækvivalent af JShell-værktøjet til Java 9 og derover og giver os mulighed for at indtaste Clojure-kode og se resultatet med det samme direkte. Dette er en fantastisk måde at eksperimentere og opdage, hvordan bestemte sprogfunktioner fungerer.

Når REPL er indlæst, får vi en prompt, hvor enhver standard Clojure-kode kan indtastes og straks udføres. Dette inkluderer enkle Clojure-konstruktioner samt interaktion med andre Java-biblioteker - selvom de skal være tilgængelige på klassestien for at blive indlæst.

Spørgsmålet fra REPL er en indikation af det aktuelle navneområde, vi arbejder i. For størstedelen af ​​vores arbejde er dette bruger navneområde, og prompten bliver således:

bruger =>

Alt i resten af ​​denne artikel antager, at vi har adgang til Clojure REPL og alle vil arbejde direkte i ethvert sådant værktøj.

4. Grundlæggende sprog

Clojure-sproget ser meget anderledes ud end mange andre JVM-baserede sprog og vil muligvis virke meget usædvanligt til at begynde med. Det anses for at være en dialekt af Lisp og har meget lignende syntaks og funktionalitet til andre Lisp-sprog.

Meget af koden, som vi skriver i Clojure - som med andre Lisp-dialekter - udtrykkes i form af lister. Lister kan derefter evalueres for at producere resultater - enten i form af flere lister eller enkle værdier.

For eksempel:

(+ 1 2) ; = 3

Dette er en liste bestående af tre elementer. “+” Symbolet angiver, at vi udfører dette opkald - tilføjelse. De resterende elementer bruges derefter med dette opkald. Således vurderes dette til “1 + 2”.

Ved at bruge en liste-syntaks her kan dette udvides trivielt. For eksempel kan vi gøre:

(+ 1 2 3 4 5) ; = 15

Og dette evalueres til “1 + 2 + 3 + 4 + 5”.

Bemærk også semikolontegnet. Dette bruges i Clojure til at angive en kommentar og er ikke slutningen på udtrykket, som vi ville se på Java.

4.1. Enkel Typer

Clojure er bygget oven på JVM, og som sådan har vi adgang til de samme standardtyper som enhver anden Java-applikation. Typer udledes typisk automatisk og behøver ikke at angives eksplicit.

For eksempel:

123; Lang 1,23; Dobbelt "Hej"; String sand; Boolsk

Vi kan også specificere nogle mere komplicerede typer ved hjælp af specielle præfikser eller suffikser:

42N; clojure.lang.BigInt 3.14159M; java.math.BigDecimal 1/3; clojure.lang.Ratio # "[A-Za-z] +"; java.util.regex.Mønster

Bemærk, at clojure.lang.BigInt type bruges i stedet for java.math.BigInteger. Dette skyldes, at Clojure-typen har nogle mindre optimeringer og rettelser.

4.2. Nøgleord og symboler

Clojure giver os begrebet både nøgleord og symboler. Nøgleord refererer kun til sig selv og bruges ofte til ting som kortnøgler. Symboler er derimod navne, der bruges til at henvise til andre ting. For eksempel er variable definitioner og funktionsnavne symboler.

Vi kan konstruere nøgleord ved hjælp af et navn, der er forud for et kolon:

bruger =>: kw: kw bruger =>: a: a

Nøgleord har direkte lighed med sig selv og ikke med noget andet:

bruger => (=: a: a) sand bruger => (=: a: b) falsk bruger => (=: a "a") falsk

De fleste andre ting i Clojure, der ikke er enkle værdier, betragtes som symboler. Disse vurderer, hvad de end henviser til, hvorimod et nøgleord altid vurderer for sig selv:

bruger => (def en 1) # 'bruger / en bruger =>: a: en bruger => en 1

4.3. Navneområder

Clojure-sproget har begrebet navneområder til organisering af vores kode. Hvert stykke kode, vi skriver, bor i et navneområde.

Som standard kører REPL i bruger navneområde - som det ses af meddelelsen "bruger =>".

Vi kan oprette og ændre navneområder ved hjælp af ns nøgleord:

bruger => (ns new.ns) nil new.ns =>

Når vi har ændret navneområder, er alt, hvad der er defineret i det gamle, ikke længere tilgængeligt for os, og noget, der er defineret i det nye, er nu tilgængeligt.

Vi kan få adgang til definitioner på tværs af navneområder ved at kvalificere dem fuldt ud. For eksempel navneområdet clojure.streng definerer en funktion store bogstaver.

Hvis vi er i clojure.streng navneområde, kan vi få adgang til det direkte. Hvis vi ikke er det, er vi nødt til at kvalificere det som clojure. snor / store bogstaver:

bruger => (clojure.string / store bogstaver "hej") "HELLO" bruger => (store bogstaver "hej"); Dette er ikke synligt i "bruger" navneområdet Syntaksfejl ved kompilering ved (REPL: 1: 1). Kan ikke løse symbolet: store bogstaver i denne sammenhæng bruger => (ns clojure.string) nil clojure.string => (store bogstaver "hej"); Dette er synligt, fordi vi nu er i "clojure.string" navneområdet "HELLO"

Vi kan også bruge krævenøgleord for at få adgang til definitioner fra et andet navneområde på en lettere måde. Der er to hovedmåder, vi kan bruge dette på - at definere et navneområde med et kortere navn, så det er lettere at bruge, og få adgang til definitioner fra et andet navneområde uden noget præfiks direkte:

clojure.string => (kræver '[clojure.string: som str]) nul clojure.string => (str / store bogstaver "Hej") "HELLO" bruger => (kræv' [clojure.string: as str: henvise [store bogstaver]]) nul bruger => (store bogstaver "Hej") "HELLO"

Begge disse påvirker kun det aktuelle navneområde, så at skifte til et andet bliver nødvendigt at have nyt kræver. Dette hjælper med at holde vores navneområder renere og give os kun adgang til det, vi har brug for.

4.4. Variabler

Når vi ved, hvordan vi definerer enkle værdier, kan vi tildele dem til variabler. Vi kan gøre dette ved hjælp af nøgleordet def:

bruger => (def en 123) # 'bruger / a

Når vi har gjort dette, kan vi bruge symbolet -enhvor som helst vi vil repræsentere denne værdi:

bruger => en 123

Variable definitioner kan være så enkle eller så komplicerede som vi ønsker.

For at definere en variabel som summen af ​​tal kan vi f.eks. Gøre:

bruger => (def b (+ 1 2 3 4 5)) # 'bruger / b bruger => b 15

Bemærk, at vi aldrig behøver at erklære variablen eller angive, hvilken type den er. Clojure bestemmer automatisk alt dette for os.

Hvis vi prøver at bruge en variabel, der ikke er defineret, får vi i stedet en fejl:

bruger => ukendt Syntaksfejl ved kompilering ved (REPL: 0: 0). Kan ikke løse symbolet: ukendt i denne sammenhæng bruger => (def c (+ 1 ukendt)) Syntaksfejl ved kompilering ved (REPL: 1: 8). Kan ikke løse symbolet: ukendt i denne sammenhæng

Bemærk, at output fra def funktion ser lidt anderledes ud end input. Definition af en variabel -en returnerer en streng af ‘Bruger / a. Dette skyldes, at resultatet er et symbol, og dette symbol er defineret i det aktuelle navneområde.

4.5. Funktioner

Vi har allerede set et par eksempler på, hvordan man kalder funktioner i Clojure. Vi opretter en liste, der starter med den funktion, der skal kaldes, og derefter alle parametrene.

Når denne liste evalueres, får vi returværdien fra funktionen. For eksempel:

bruger => (java.time.Instant / nu) #objekt [java.time.Instant 0x4b6690c0 "2019-01-15T07: 54: 01.516Z"] bruger => (java.time.Instant / parse "2019-01- 15T07: 55: 00Z ") #object [java.time.Instant 0x6b8d96d9" 2019-01-15T07: 55: 00Z "] bruger => (java.time.OffsetDateTime / af 2019 01 15 7 56 0 0 java.time. ZoneOffset / UTC) #object [java.time.OffsetDateTime 0xf80945f "2019-01-15T07: 56Z"]

Vi kan også rede opkald til funktioner, for når vi vil sende output fra et funktionsopkald ind som en parameter til en anden:

bruger => (java.time.OffsetDateTime / af 2018 01 15 7 57 0 0 (java.time.ZoneOffset / ofHours -5)) #objekt [java.time.OffsetDateTime 0x1cdc4c27 "2018-01-15T07: 57-05: 00 "]

Også, vi kan også definere vores funktioner hvis vi ønsker det. Funktioner oprettes ved hjælp af fn kommando:

bruger => (fn [a b] (println "Tilføjelse af numre" a "og" b) (+ a b)) #objekt [bruger $ eval165 $ fn__166 0x5644dc81 "[e-mailbeskyttet]"]

Desværre, dette giver ikke funktionen et navn, der kan bruges. I stedet kan vi definere et symbol, der repræsenterer denne funktion ved hjælp af def, nøjagtigt som vi har set for variabler:

bruger => (def tilføj (fn [a b] (println "Tilføjelse af numre" a "og" b) (+ a b))) # 'bruger / tilføj

Nu hvor vi har defineret denne funktion, kan vi kalde den den samme som enhver anden funktion:

bruger => (tilføj 1 2) Tilføj nummer 1 og 2 3

Som en bekvemmelighed Clojure tillader os også at bruge defn for at definere en funktion med et navn på én gang.

For eksempel:

bruger => (defn sub [a b] (println "Subtrahere" b "fra" a) (- a b)) # 'bruger / sub user => (sub 5 2) Subtrahere 2 fra 5 3

4.6. Let og lokale variabler

Det def opkald definerer et symbol, der er globalt i det aktuelle navneområde. Dette er typisk ikke det, der ønskes ved udførelse af kode. I stedet, Clojure tilbyder lade kald for at definere variabler, der er lokale for en blok. Dette er især nyttigt, når du bruger dem i funktioner, hvor du ikke vil have, at variablerne lækker uden for funktionen.

For eksempel kunne vi definere vores underfunktion:

bruger => (defn sub [a b] (def resultat (- a b)) (println "Resultat:" resultat) resultat) # 'bruger / sub

Brug af dette har dog følgende uventede bivirkning:

bruger => (sub 1 2) Resultat: -1 -1 bruger => resultat; Stadig synlig uden for funktionen -1

Lad os i stedet omskrive det ved hjælp af lade:

bruger => (defn sub [ab] (lad [resultat (- ab)] (println "Resultat:" resultat) resultat)) # 'bruger / sub bruger => (sub 1 2) Resultat: -1-1 bruger = > resultat Syntaksfejl ved kompilering ved (REPL: 0: 0). Kan ikke løse symbolet: resultat i denne sammenhæng

Denne gang resultat symbolet er ikke synligt uden for funktionen. Eller faktisk uden for lade blok, hvor den blev brugt.

5. Samlinger

Indtil videre har vi for det meste interageret med enkle værdier. Vi har også set lister, men intet mere. Clojure har dog et komplet sæt samlinger, der dog kan bruges, bestående af lister, vektorer, kort og sæt:

  • En vektor er en ordnet liste over værdier - enhver vilkårlig værdi kan anbringes i en vektor inklusive andre samlinger.
  • Et sæt er en uordnet samling af værdier og kan aldrig indeholde den samme værdi mere end én gang.
  • Et kort er et simpelt sæt nøgle / værdipar. Det er meget almindeligt at bruge nøgleord som nøglerne på et kort, men vi kan bruge enhver værdi, vi kan lide, inklusive andre samlinger.
  • En liste svarer meget til en vektor. Forskellen svarer til forskellen mellem en ArrayList og en LinkedList i Java. Typisk foretrækkes en vektor, men en liste er bedre, hvis vi vil føje elementer til starten, eller hvis vi kun nogensinde vil have adgang til elementerne i rækkefølge.

5.1. Konstruktion af samlinger

Oprettelse af hver af disse kan gøres ved hjælp af en stenografisk notation eller ved hjælp af et funktionsopkald:

; Vektorbruger => [1 2 3] [1 2 3] bruger => (vektor 1 2 3) [1 2 3]; Listebruger => '(1 2 3) (1 2 3) bruger => (liste 1 2 3) (1 2 3); Indstil bruger => # {1 2 3} # {1 3 2} bruger => (hash-sæt 1 2 3) # {1 3 2}; Kortbruger => {: a 1: b 2} {: a 1,: b 2} bruger => (hash-map: a 1: b 2) {: b 2,: a 1}

Bemærk, at Sæt og Kort eksempler returnerer ikke værdierne i samme rækkefølge. Dette skyldes, at disse samlinger i sagens natur er uordnede, og hvad vi ser, afhænger af, hvordan de er repræsenteret i hukommelsen.

Vi kan også se, at syntaksen til oprettelse af en liste er meget lig den almindelige Clojure-syntaks for udtryk. Et Clojure-udtryk er faktisk en liste, der bliver evalueret, hvorimod apostroftegnet her indikerer, at vi ønsker den faktiske liste over værdier i stedet for at evaluere den.

Vi kan selvfølgelig tildele en samling til en variabel på samme måde som enhver anden værdi. Vi kan også bruge en samling som en nøgle eller værdi i en anden samling.

Lister betragtes som en sekv. Dette betyder, at klassen implementerer ISeq interface. Alle andre samlinger kan konverteres til en sekv bruger sekv fungere:

bruger => (seq [1 2 3]) (1 2 3) bruger => (seq # {1 2 3}) (1 3 2) bruger => (seq {: a 1 2 3}) ([: a 1] [2 3])

5.2. Adgang til samlinger

Når vi har en samling, kan vi interagere med den for at få værdierne ud igen. Hvordan vi kan gøre dette afhænger lidt af den pågældende samling, da hver af dem har forskellig semantik.

Vektorer er den eneste samling, der lader os få en vilkårlig værdi efter indeks. Dette gøres ved at evaluere vektoren og indekset som et udtryk:

bruger => (min-vektor 2); [1 2 3] 3

Vi kan også gøre det ved hjælp af den samme syntaks for kort:

bruger => (mit kort: b) 2

Vi har også funktioner til at få adgang til vektorer og lister for at få den første værdi, sidste værdi og resten af ​​listen:

bruger => (første min-vektor) 1 bruger => (sidste min-liste) 3 bruger => (næste min-vektor) (2 3)

Kort har yderligere funktioner til at få hele listen over nøgler og værdier:

bruger => (nøgler my-map) (: a: b) user => (vals my-map) (1 2)

Den eneste reelle adgang, som vi har til sæt, er at se, om et bestemt element er medlem.

Dette ligner meget adgang til enhver anden samling:

bruger => (mit sæt 1) 1 bruger => (mit sæt 5) nul

5.3. Identificering af samlinger

Vi har set, at den måde, vi får adgang til en samling på, varierer afhængigt af den type samling, vi har. Vi har et sæt funktioner, vi kan bruge til at bestemme dette, både på en specifik og mere generisk måde.

Hver af vores samlinger har en bestemt funktion til at bestemme, om en given værdi er af den type - liste? til lister, sæt? til sæt og så videre. Derudover er der seq? til bestemmelse af, om en given værdi er en sekv af enhver art, og associerende? for at bestemme, om en given værdi tillader associativ adgang af enhver art - hvilket betyder vektorer og kort:

bruger => (vektor? [1 2 3]); En vektor er en vektor sand bruger => (vektor? # {1 2 3}); Et sæt er ikke en vektor falsk bruger => (liste? '(1 2 3)); En liste er en liste sand bruger => (liste? [1 2 3]); En vektor er ikke en liste falsk bruger => (kort? {: A 1: b 2}); Et kort er en kort sand bruger => (kort? # {1 2 3}); Et sæt er ikke en kort falsk bruger => (seq? '(1 2 3)); En liste er en seq sand bruger => (seq? [1 2 3]); En vektor er ikke en seq falsk bruger => (seq? (Seq [1 2 3])); En vektor kan konverteres til en seq sand bruger => (associativ? {: A 1: b 2}); Et kort er associativ sand bruger => (associativ? [1 2 3]); En vektor er associativ sand bruger => (associativ? '(1 2 3)); En liste er ikke associerende falsk

5.4. Muterende samlinger

I Clojure, som med de fleste funktionelle sprog, er alle samlinger uforanderlige. Alt, hvad vi gør for at ændre en samling, resulterer i, at en helt ny samling oprettes for at repræsentere ændringerne. Dette kan give enorme effektivitetsfordele og betyder, at der ikke er nogen risiko for utilsigtede bivirkninger.

Vi skal dog også være forsigtige med at forstå dette, ellers vil de forventede ændringer i vores samlinger ikke ske.

Tilføjelse af nye elementer til en vektor, liste eller sæt sker ved hjælp af konj. Dette fungerer forskelligt i hvert af disse tilfælde, men med den samme grundlæggende intention:

bruger => (conj [1 2 3] 4); Føjes til slutningen [1 2 3 4] bruger => (conj '(1 2 3) 4); Føjes til begyndelsen (4 1 2 3) bruger => (konj # {1 2 3} 4); Uordnet # {1 4 3 2} bruger => (konj # {1 2 3} 3); Tilføjelse af en allerede tilstedeværende post betyder intet # {1 3 2}

Vi kan også fjerne poster fra et sæt ved hjælp af disj. Bemærk, at dette ikke fungerer på en liste eller vektor, fordi de er strengt ordnet:

bruger => (afvis nr. {1 2 3} 2); Fjerner post nr. {1 3} bruger => (vis nr. {1 2 3} 4); Gør intet, fordi posten ikke var til stede # {1 3 2}

Tilføjelse af nye elementer til et kort sker ved hjælp af assoc. Vi kan også fjerne poster fra et kort ved hjælp af dissocere:

bruger => (assoc {: a 1: b 2}: c 3); Tilføjer en ny nøgle {: a 1,: b 2,: c 3} bruger => (assoc {: a 1: b 2}: b 3); Opdaterer en eksisterende nøgle {: a 1,: b 3} bruger => (dissoc {: a 1: b 2}: b); Fjerner en eksisterende nøgle {: a 1} bruger => (dissoc {: a 1: b 2}: c); Gør intet, fordi nøglen ikke var til stede {: a 1,: b 2}

5.5. Funktionelle programmeringskonstruktioner

Clojure er kernen i et funktionelt programmeringssprog. Det betyder at vi har adgang til mange traditionelle funktionelle programmeringskoncepter - f.eks kort, filter, og reducere. Disse fungerer generelt det samme som på andre sprog. Den nøjagtige syntaks kan dog være lidt anderledes.

Specifikt tager disse funktioner generelt den funktion, der skal anvendes som det første argument, og samlingen, der skal anvendes til det som det andet argument:

bruger => (map inc [1 2 3]); Forøg hver værdi i vektoren (2 3 4) bruger => (kort inkl. Nr. {1 2 3}); Forøg hver værdi i sættet (2 4 3) bruger => (filter ulige? [1 2 3 4 5]); Returner kun ulige værdier (1 3 5) bruger => (fjern ulige? [1 2 3 4 5]); Returner kun ikke-ulige værdier (2 4) bruger => (reducer + [1 2 3 4 5]); Tilføj alle værdierne sammen, og returner summen 15

6. Kontrolstrukturer

Som med alle sprog til generelle formål kræver Clojure opkald til standardkontrolstrukturer, såsom betingede og sløjfer.

6.1. Betingelser

Betingelser håndteres af hvis udmelding. Dette tager tre parametre: en test, en blok, der skal udføres, hvis testen er rigtigt, og en blok, der skal udføres, hvis testen er falsk. Hver af disse kan være en simpel værdi eller en standardliste, der evalueres efter behov:

bruger => (hvis sand 1 2) 1 bruger => (hvis falsk 1 2) 2

Vores test kan overhovedet være alt, hvad vi har brug for - det behøver ikke at være en sandt falsk værdi. Det kan også være en blok, der evalueres for at give os den værdi, vi har brug for:

bruger => (hvis (> 1 2) "Sand" "Falsk") "Falsk"

Alle standardchecks inklusive =, >, og <, kan bruges her. Der er også et sæt prædikater, der kan bruges af forskellige andre grunde - vi så nogle allerede, når vi ser på samlinger, for eksempel:

bruger => (hvis (ulige? 1) "1 er ulige" "1 er lige") "1 er ulige"

Testen kan overhovedet returnere enhver værdi - det behøver ikke kun at være rigtigt eller falsk. Imidlertid anses det for at være rigtigt hvis værdien er noget undtagen falsk eller nul. Dette adskiller sig fra den måde, JavaScript fungerer, hvor der er et stort sæt værdier, der betragtes som "sandhed-y", men ikke rigtigt:

user => (if 0 "True" "False") "True" user => (if [] "True" "False") "True" user => (if nul "True" "False") "False"

6.2. Looping

Vores funktionelle support til samlinger håndterer meget af looping-arbejdet - i stedet for at skrive en sløjfe over samlingen bruger vi standardfunktionerne og lader sproget gøre iteration for os.

Uden for dette sker looping udelukkende ved hjælp af rekursion. Vi kan skrive rekursive funktioner, eller vi kan bruge løkke og gentage sig nøgleord til at skrive en rekursiv stil loop:

bruger => (loop [accum [] i 0] (hvis (= i 10) accum (recur (conj accum i) (inc i)))) [0 1 2 3 4 5 6 7 8 9]

Det løkke opkald starter en indre blok, der udføres på hver iteration og starter med at indstille nogle indledende parametre. Det gentage sig opkald kalder derefter tilbage i sløjfen og giver de næste parametre, der skal bruges til iterationen. Hvis gentage sig kaldes ikke, så slutter sløjfen.

I dette tilfælde løkker vi hver gang, at jeg værdi er ikke lig med 10, og så snart den er lig med 10, returnerer vi i stedet den akkumulerede talvektor.

7. Resume

Denne artikel har givet en introduktion til Clojure-programmeringssproget og viser, hvordan syntaksen fungerer, og nogle af de ting, du kan gøre med det. Dette er kun et indledende niveau og går ikke ned i dybden på alt, hvad der kan gøres med sproget.

Men hvorfor ikke hente det, give det en chance og se, hvad du kan gøre med det.


$config[zx-auto] not found$config[zx-overlay] not found