Introduksjon til Clojure

1. Introduksjon

Clojure er et funksjonelt programmeringsspråk som kjører helt på Java Virtual Machine, på samme måte som Scala og Kotlin. Clojure anses å være et Lisp-derivat og vil være kjent for alle som har erfaring med andre Lisp-språk.

Denne opplæringen gir en introduksjon til Clojure-språket, og introduserer hvordan du kommer i gang med det, og noen av nøkkelbegrepene for hvordan det fungerer.

2. Installere Clojure

Clojure er tilgjengelig som installatører og praktiske skript for bruk på Linux og macOS. Dessverre, på dette stadiet, har Windows ikke et slikt installasjonsprogram.

Imidlertid kan Linux-skriptene fungere i noe som Cygwin eller Windows Bash. Det er også en online tjeneste som kan brukes til å teste ut språket, og eldre versjoner har en frittstående versjon som kan brukes.

2.1. Frittstående nedlasting

Den frittstående JAR-filen kan lastes ned fra Maven Central. Dessverre fungerer versjoner som er nyere enn 1.8.0 ikke lenger på denne måten på grunn av at JAR-filen er delt inn i mindre moduler.

Når denne JAR-filen er lastet ned, kan vi bruke den som en interaktiv REPL ved å behandle den som en kjørbar JAR:

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

2.2. Webgrensesnitt til REPL

Et webgrensesnitt til Clojure REPL er tilgjengelig på //repl.it/languages/clojure slik at vi kan prøve uten å måtte laste ned noe. Foreløpig støtter dette bare Clojure 1.8.0 og ikke de nyere utgivelsene.

2.3. Installatør på MacOS

Hvis du bruker macOS og har Homebrew installert, kan den siste utgivelsen av Clojure enkelt installeres:

$ brew install clojure

Dette vil støtte den nyeste versjonen av Clojure - 1.10.0 i skrivende stund. Når den er installert, kan vi laste inn REPL ved å bruke clojure eller clj kommandoer:

$ clj Clojure 1.10.0 bruker =>

2.4. Installatør på Linux

Et selvinstallerende skallskript er tilgjengelig for å installere verktøyene 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-installasjonsprogrammet, vil disse være tilgjengelige for de nyeste versjonene av Clojure og kan kjøres ved hjelp av clojure eller clj kommandoer.

3. Introduksjon til Clojure REPL

Alle alternativene ovenfor gir oss tilgang til Clojure REPL. Dette er den direkte Clojure-ekvivalenten til JShell-verktøyet for Java 9 og nyere, og lar oss legge inn Clojure-kode og se resultatet umiddelbart direkte. Dette er en fantastisk måte å eksperimentere og oppdage hvordan visse språkfunksjoner fungerer.

Når REPL er lastet inn, får vi en melding der enhver standard Clojure-kode kan legges inn og kjøres umiddelbart. Dette inkluderer enkle Clojure-konstruksjoner, samt interaksjon med andre Java-biblioteker - selv om de må være tilgjengelige på klassestien for å bli lastet.

Ledeteksten til REPL er en indikasjon på det nåværende navneområdet vi jobber i. For det meste av vårt arbeid er dette bruker navneområdet, og ledeteksten vil være:

bruker =>

Alt i resten av denne artikkelen vil anta at vi har tilgang til Clojure REPL, og vil alle fungere direkte i et slikt verktøy.

4. Grunnleggende språk

Clojurespråket ser veldig annerledes ut enn mange andre JVM-baserte språk, og vil muligens virke veldig uvanlig til å begynne med. Det anses å være en dialekt av Lisp og har veldig lik syntaks og funksjonalitet til andre Lisp-språk.

Mye av koden som vi skriver i Clojure - som med andre Lisp-dialekter - kommer til uttrykk i form av lister. Lister kan deretter evalueres for å gi resultater - enten i form av flere lister eller enkle verdier.

For eksempel:

(+ 1 2) ; = 3

Dette er en liste som består av tre elementer. “+” Symbolet indikerer at vi utfører denne samtalen - tillegg. De resterende elementene blir deretter brukt med denne samtalen. Dermed vurderes dette til “1 + 2”.

Ved å bruke en liste-syntaks her kan dette utvides trivielt. For eksempel kan vi gjøre:

(+ 1 2 3 4 5) ; = 15

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

Legg også merke til semikolontegn. Dette brukes i Clojure for å indikere en kommentar og er ikke slutten på uttrykket som vi ser på Java.

4.1. Enkel Typer

Clojure er bygget på toppen av JVM, og som sådan har vi tilgang til de samme standardtypene som alle andre Java-applikasjoner. Typer utledes vanligvis automatisk og trenger ikke spesifiseres eksplisitt.

For eksempel:

123; Lang 1,23; Dobbel "Hei"; String sant; Boolsk

Vi kan også spesifisere noen mer kompliserte typer ved å bruke spesielle prefikser eller suffikser:

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

Merk at clojure.lang.BigInt type brukes i stedet for java.math.BigInteger. Dette er fordi Clojure-typen har noen mindre optimaliseringer og reparasjoner.

4.2. Nøkkelord og symboler

Clojure gir oss konseptet med både nøkkelord og symboler. Nøkkelord refererer bare til seg selv og brukes ofte til ting som kartnøkler. Symboler er derimot navn som brukes til å referere til andre ting. For eksempel er variabeldefinisjoner og funksjonsnavn symboler.

Vi kan konstruere nøkkelord ved å bruke et navn foran et kolon:

bruker =>: kw: kw bruker =>: a: a

Nøkkelord har direkte likhet med seg selv, og ikke med noe annet:

user => (=: a: a) true user => (=: a: b) false user => (=: a "a") false

De fleste andre ting i Clojure som ikke er enkle verdier anses å være symboler. Disse evaluerer til hva de refererer til, mens et nøkkelord alltid evaluerer for seg selv:

bruker => (def a 1) # 'user / a user =>: a: a user => a 1

4.3. Navneplasser

Clojurespråket har begrepet navnerom for å organisere koden vår. Hver kode vi skriver bor i et navneområde.

Som standard kjører REPL i bruker navneområde - sett av ledeteksten "user =>".

Vi kan opprette og endre navneområder ved hjelp av ns nøkkelord:

bruker => (ns new.ns) null new.ns =>

Når vi har endret navnerom, er alt som er definert i det gamle ikke lenger tilgjengelig for oss, og alt som er definert i det nye er nå tilgjengelig.

Vi kan få tilgang til definisjoner på tvers av navnerom ved å fullføre dem. For eksempel navneplassen clojure.streng definerer en funksjon stor bokstav.

Hvis vi er i clojure.streng navneområdet, kan vi få tilgang til det direkte. Hvis vi ikke er det, må vi kvalifisere det som clojure. snor / store bokstaver:

bruker => (clojure.string / store bokstaver "hei") "HELLO" bruker => (store bokstaver "hei"); Dette er ikke synlig i "bruker" -navnet Syntaksfeil ved kompilering av (REPL: 1: 1). Kan ikke løse symbolet: store bokstaver i denne sammenhengen bruker => (ns clojure.string) null clojure.string => (store bokstaver "hallo"); Dette er synlig fordi vi nå er i "clojure.string" -navnet "HELLO"

Vi kan også bruke krevernøkkelord for å få tilgang til definisjoner fra et annet navneområde på en enklere måte. Det er to hovedmåter vi kan bruke dette på - å definere et navneområde med et kortere navn slik at det er lettere å bruke, og å få tilgang til definisjoner fra et annet navneområde uten noe prefiks direkte:

clojure.string => (krever '[clojure.string: as str]) null clojure.string => (str / store bokstaver "Hello") "HELLO" bruker => (krever' [clojure.string: as str: referer [store bokstaver]]) ingen brukere => (store bokstaver "Hei") "HELLO"

Begge disse påvirker bare det nåværende navneområdet, så det å bytte til et annet må ha nytt krever. Dette bidrar til å holde navnene våre renere og gi oss tilgang til bare det vi trenger.

4.4. Variabler

Når vi vet hvordan vi skal definere enkle verdier, kan vi tilordne dem til variabler. Vi kan gjøre dette ved å bruke nøkkelordet def:

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

Når vi har gjort dette, kan vi bruke symbolet enhvor som helst vi vil representere denne verdien:

bruker => a 123

Variable definisjoner kan være så enkle eller så kompliserte som vi vil.

For eksempel, for å definere en variabel som summen av tall, kan vi gjøre:

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

Legg merke til at vi aldri trenger å erklære variabelen eller indikere hvilken type den er. Clojure bestemmer automatisk alt dette for oss.

Hvis vi prøver å bruke en variabel som ikke er definert, vil vi i stedet få en feil:

bruker => ukjent Syntaksfeil ved kompilering ved (REPL: 0: 0). Kan ikke løse symbolet: ukjent i denne sammenhengen bruker => (def c (+ 1 ukjent)) Syntaksfeil ved kompilering ved (REPL: 1: 8). Kan ikke løse symbolet: ukjent i denne sammenhengen

Legg merke til at produksjonen av def funksjonen ser litt annerledes ut enn inngangen. Definere en variabel en returnerer en streng av ‘Bruker / a. Dette er fordi resultatet er et symbol, og dette symbolet er definert i det nåværende navneområdet.

4.5. Funksjoner

Vi har allerede sett et par eksempler på hvordan du kan ringe funksjoner i Clojure. Vi lager en liste som starter med funksjonen som skal kalles, og deretter alle parametrene.

Når denne listen evalueres, får vi returverdien fra funksjonen. For eksempel:

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

Vi kan også hekke anrop til funksjoner, for når vi vil overføre utdataene fra en funksjonsanrop som en parameter til en annen:

bruker => (java.time.OffsetDateTime / av 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 funksjonene våre hvis vi ønsker det. Funksjoner opprettes ved hjelp av fn kommando:

bruker => (fn [a b] (println "Legge til tall" a "og" b) (+ a b)) #objekt [bruker $ eval165 $ fn__166 0x5644dc81 "[e-postbeskyttet]"]

Dessverre, dette gir ikke funksjonen et navn som kan brukes. I stedet kan vi definere et symbol som representerer denne funksjonen ved hjelp av def, akkurat som vi har sett for variabler:

bruker => (def add (fn [a b] (println "Legge til tall" a "og" b) (+ a b))) # 'bruker / add

Nå som vi har definert denne funksjonen, kan vi kalle den den samme som enhver annen funksjon:

bruker => (legg til 1 2) Legge til nummer 1 og 2 3

Som en bekvemmelighet, Clojure lar oss også bruke defn for å definere en funksjon med et navn på en gang.

For eksempel:

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

4.6. La og lokale variabler

De def call definerer et symbol som er globalt i det gjeldende navneområdet. Dette er vanligvis ikke det som er ønsket når du utfører kode. I stedet, Clojure tilbyr la ring for å definere variabler lokale til en blokk. Dette er spesielt nyttig når du bruker dem i funksjoner, der du ikke vil at variablene skal lekke utenfor funksjonen.

For eksempel kan vi definere underfunksjonen vår:

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

Imidlertid har bruk av dette følgende uventede bivirkning:

bruker => (sub 1 2) Resultat: -1 -1 bruker => resultat; Fremdeles synlig utenfor funksjonen -1

I stedet, la oss skrive det på nytt med la:

bruker => (defn sub [ab] (la [resultat (- ab)] (println "Resultat:" resultat) resultat)) # 'bruker / underbruker => (sub 1 2) Resultat: -1-1 bruker = > resultat Syntaksfeil ved kompilering ved (REPL: 0: 0). Kan ikke løse symbolet: resultat i denne sammenhengen

Denne gangen resultat symbolet er ikke synlig utenfor funksjonen. Eller, faktisk, utenfor la blokk der den ble brukt.

5. Samlinger

Så langt har vi mest samhandlet med enkle verdier. Vi har sett lister også, men ikke noe mer. Clojure har et komplett sett med samlinger som kan brukes, men består av lister, vektorer, kart og sett:

  • En vektor er en ordnet liste over verdier - en vilkårlig verdi kan settes i en vektor, inkludert andre samlinger.
  • Et sett er en uordnet samling verdier, og kan aldri inneholde den samme verdien mer enn en gang.
  • Et kart er et enkelt sett med nøkkel / verdipar. Det er veldig vanlig å bruke nøkkelord som nøklene på et kart, men vi kan bruke hvilken som helst verdi vi liker, inkludert andre samlinger.
  • En liste er veldig lik en vektor. Forskjellen er lik den mellom en ArrayList og en LinkedList i Java. Vanligvis foretrekkes en vektor, men en liste er bedre hvis vi vil legge til elementer i starten, eller hvis vi bare noen gang vil ha tilgang til elementene i sekvensiell rekkefølge.

5.1. Konstruksjon av samlinger

Å lage hver av disse kan gjøres ved hjelp av en stenografisk notasjon eller ved hjelp av en funksjonsanrop:

; Vektorbruker => [1 2 3] [1 2 3] bruker => (vektor 1 2 3) [1 2 3]; Listebruker => '(1 2 3) (1 2 3) bruker => (liste 1 2 3) (1 2 3); Angi bruker => # {1 2 3} # {1 3 2} bruker => (hash-sett 1 2 3) # {1 3 2}; Kartbruker => {: a 1: b 2} {: a 1,: b 2} bruker => (hash-map: a 1: b 2) {: b 2,: a 1}

Legg merke til at Sett og Kart eksempler returnerer ikke verdiene i samme rekkefølge. Dette er fordi disse samlingene iboende er uordnet, og det vi ser avhenger av hvordan de er representert i minnet.

Vi kan også se at syntaksen for å lage en liste er veldig lik den vanlige Clojure-syntaksen for uttrykk. Et Clojure-uttrykk er faktisk en liste som blir evaluert, mens apostroftegnet her indikerer at vi ønsker den faktiske listen over verdier i stedet for å evaluere den.

Vi kan selvfølgelig tildele en samling til en variabel på samme måte som enhver annen verdi. Vi kan også bruke en samling som en nøkkel eller verdi i en annen samling.

Lister anses å være en seq. Dette betyr at klassen implementerer ISeq grensesnitt. Alle andre samlinger kan konverteres til en seq bruker seq funksjon:

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

5.2. Få tilgang til samlinger

Når vi har en samling, kan vi samhandle med den for å få verdiene ut igjen. Hvordan vi kan gjøre dette avhenger litt av den aktuelle samlingen, siden hver av dem har forskjellig semantikk.

Vektorer er den eneste samlingen som lar oss få en vilkårlig verdi etter indeks. Dette gjøres ved å evaluere vektoren og indeksen som et uttrykk:

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

Vi kan gjøre det samme, ved hjelp av samme syntaks, også for kart:

bruker => (my-map: b) 2

Vi har også funksjoner for å få tilgang til vektorer og lister for å få den første verdien, siste verdien og resten av listen:

bruker => (første min-vektor) 1 bruker => (siste min-liste) 3 bruker => (neste min-vektor) (2 3)

Kart har tilleggsfunksjoner for å få hele listen over nøkler og verdier:

bruker => (taster my-map) (: a: b) user => (vals my-map) (1 2)

Den eneste reelle tilgangen vi har til sett er å se om et bestemt element er medlem.

Dette ser veldig ut som tilgang til andre samlinger:

bruker => (mitt sett 1) ​​1 bruker => (mitt sett 5) null

5.3. Identifisere samlinger

Vi har sett at måten vi får tilgang til en samling på, varierer avhengig av hvilken type samling vi har. Vi har et sett med funksjoner vi kan bruke til å bestemme dette, både på en spesifikk og mer generisk måte.

Hver av våre samlinger har en bestemt funksjon for å avgjøre om en gitt verdi er av den typen - liste? for lister, sett? for sett og så videre. I tillegg er det sekvens? for å bestemme om en gitt verdi er a seq av noe slag, og assosiativ? for å bestemme om en gitt verdi tillater tilknytningstilgang av noe slag - som betyr vektorer og kart:

bruker => (vektor? [1 2 3]); En vektor er en vektorsann bruker => (vektor? # {1 2 3}); Et sett er ikke en vektor falsk bruker => (liste? '(1 2 3)); En liste er en liste sann bruker => (liste? [1 2 3]); En vektor er ikke en liste falsk bruker => (kart? {: A 1: b 2}); Et kart er en map true user => (map? # {1 2 3}); Et sett er ikke en map false user => (seq? '(1 2 3)); En liste er en ekte bruker => (lik? [1 2 3]); En vektor er ikke en seq falsk bruker => (seq? (Seq [1 2 3])); En vektor kan konverteres til en ekte bruker => (assosiativ? {: A 1: b 2}); Et kart er assosiativ ekte bruker => (assosiativ? [1 2 3]); En vektor er assosiativ sann bruker => (assosiativ? '(1 2 3)); En liste er ikke assosiativ falsk

5.4. Muterende samlinger

I Clojure, som med de fleste funksjonelle språk, er alle samlinger uforanderlige. Alt vi gjør for å endre en samling, resulterer i at en splitter ny samling blir opprettet for å representere endringene. Dette kan gi enorme effektivitetsfordeler og betyr at det ikke er noen risiko for utilsiktede bivirkninger.

Vi må imidlertid også være forsiktige med å forstå dette, ellers vil ikke forventede endringer i samlingene våre skje.

Å legge til nye elementer i en vektor, liste eller sett gjøres ved hjelp av conj. Dette fungerer annerledes i hver av disse tilfellene, men med samme grunnleggende intensjon:

bruker => (conj [1 2 3] 4); Legger til slutten [1 2 3 4] bruker => (conj '(1 2 3) 4); Legger til i begynnelsen (4 1 2 3) bruker => (konj # {1 2 3} 4); Ubestilt nr. {1 4 3 2} bruker => (konj. Nr. {1 2 3} 3); Å legge til en allerede tilstedeværende oppføring gjør ingenting # {1 3 2}

Vi kan også fjerne oppføringer fra et sett ved hjelp av disj. Merk at dette ikke fungerer på en liste eller vektor, fordi de er strengt ordnet:

bruker => (disj # {1 2 3} 2); Fjerner oppføringen nr. {1 3} bruker => (vis nr. {1 2 3} 4); Gjør ingenting fordi oppføringen ikke var til stede nr. {1 3 2}

Å legge til nye elementer på et kart gjøres ved hjelp av assoc. Vi kan også fjerne oppføringer fra et kart ved hjelp av dissocere:

bruker => (assoc {: a 1: b 2}: c 3); Legger til en ny nøkkel {: a 1,: b 2,: c 3} bruker => (assoc {: a 1: b 2}: b 3); Oppdaterer en eksisterende nøkkel {: a 1,: b 3} bruker => (dissoc {: a 1: b 2}: b); Fjerner en eksisterende nøkkel {: a 1} user => (dissoc {: a 1: b 2}: c); Gjør ingenting fordi nøkkelen ikke var til stede {: a 1,: b 2}

5.5. Funksjonell programmeringskonstruksjon

Clojure er i sitt hjerte et funksjonelt programmeringsspråk. Dette betyr at vi har tilgang til mange tradisjonelle funksjonelle programmeringskonsepter - som f.eks kart, filter, og redusere. Disse fungerer generelt det samme som på andre språk. Den eksakte syntaksen kan imidlertid være litt annerledes.

Spesielt tar disse funksjonene vanligvis funksjonen som gjelder som det første argumentet, og samlingen som den skal brukes på som det andre argumentet:

bruker => (map inc [1 2 3]); Øk hver verdi i vektoren (2 3 4) bruker => (map inc # {1 2 3}); Øk hver verdi i settet (2 4 3) bruker => (filter odd? [1 2 3 4 5]); Bare returner oddverdier (1 3 5) bruker => (fjern odd? [1 2 3 4 5]); Returner bare ikke-merkelige verdier (2 4) bruker => (reduser + [1 2 3 4 5]); Legg alle verdiene sammen, og returner summen 15

6. Kontrollstrukturer

Som med alle språk for generelle formål, har Clojure krav til standard kontrollstrukturer, for eksempel betingede og sløyfer.

6.1. Betingelser

Betingelser håndteres av hvis uttalelse. Dette tar tre parametere: en test, en blokk å utføre hvis testen er ekte, og en blokk for å utføre hvis testen er falsk. Hver av disse kan være en enkel verdi eller en standardliste som vil bli evaluert på forespørsel:

bruker => (hvis sant 1 2) 1 bruker => (hvis usant 1 2) 2

Testen vår kan være hva som helst vi trenger - den trenger ikke være en sant / usant verdi. Det kan også være en blokk som blir evaluert for å gi oss verdien vi trenger:

user => (if (> 1 2) "True" "False") "False"

Alle standard sjekker, inkludert =, >, og <, kan brukes her. Det er også et sett med predikater som kan brukes av forskjellige andre grunner - vi så noen allerede når vi så på samlinger, for eksempel:

bruker => (hvis (odd? 1) "1 er merkelig" "1 er jevn") "1 er merkelig"

Testen kan returnere hvilken som helst verdi i det hele tatt - det trenger ikke bare å være ekte eller falsk. Imidlertid anses det å være ekte hvis verdien er noe unntatt falsk eller null. Dette er forskjellig fra måten JavaScript fungerer på, der det er et stort sett med verdier som anses å være "sannhet-y", men ikke ekte:

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

6.2. Looping

Vår funksjonelle støtte på samlinger håndterer mye av loopingsarbeidet - i stedet for å skrive en løkke over samlingen, bruker vi standardfunksjonene og lar språket gjøre iterasjonen for oss.

Utenfor dette gjøres looping helt ved hjelp av rekursjon. Vi kan skrive rekursive funksjoner, eller vi kan bruke Løkke og gjentar seg nøkkelord for å skrive en rekursiv stilløkke:

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

De Løkke anrop starter en indre blokk som utføres på hver iterasjon og starter med å sette opp noen innledende parametere. De gjentar seg samtale ringer deretter tilbake i løkken, og gir de neste parametrene som skal brukes til iterasjonen. Hvis gjentar seg kalles ikke, så blir løkken ferdig.

I dette tilfellet løkker vi hver gang det Jeg verdien er ikke lik 10, og så snart den er lik 10, returnerer vi i stedet den akkumulerte tallvektoren.

7. Oppsummering

Denne artikkelen har gitt en introduksjon til Clojure-programmeringsspråket og viser hvordan syntaksen fungerer og noen av tingene du kan gjøre med den. Dette er bare et introduksjonsnivå og går ikke i dybden på alt som kan gjøres med språket.

Men hvorfor ikke hente den, gi den en sjanse og se hva du kan gjøre med den.


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