Julia (ohjelmointikieli)

**Julia** on ohjelmointikieli, jota on kehitetty erityisesti tieteelliseen laskentaan, tavoitteenaan yhdistää hitaiden dynaamisten kielten helppokäyttöisyys ja perinteisten staattisten kielten suorituskyky. 

Julian keskeisin piirre ja ohjelmointitapa on *multiple dispatch* eli funktion koko tyyppijälki määrittää, mitä toteutusta eli metodia tietystä funktiosta kutsutaan. Monissa muissa kielissä metodit kuuluvat yhdelle objektille, eli vain ensimmäisen parametrin (esim. `self`) tyypillä on merkitystä (engl. *single dispatch*). 

## Käyttöaiheet

Lineaarialgebra on yksi numeerisen laskennan keskeisistä alueista, joten tähän löytyy hyvä tuki Julian perus- ja standardikirjastosta (`using LinearAlgebra`):
* `AbstractArray` ja tämän alatyypit mahdollistavat moniulotteiset taulukot suoraan `Base`-paketista
* Monipuolinen vektori- `[1, 2, 3]` ja matriisisyntaksi `[1 2 3; 4 5 6]` 
* Matriisioperaattorit `+`, `-`, `*`, `/`, `⋅`, `×` 
* Jälki `tr(M)`, determinantti `det(M)` ja kääntäminen `inv(M)`
* Eigen-arvot `eigvals(M)`, eigen-vektorit `eigvecs(M)` ja faktorointi `factorize(M)`
* Matriisityypit kuten `Symmetric`, `Hermitian` ja `Tridiagonal`
* Faktorointityypit kuten `BunchKaufman`, `LU` ja `QR`
* Lyhennelmät `[x*y for x=1:5, y=5:10]`, jotka tekevät taulukon
* Generaattorit `(x^5 for x=1:10)`, jotka voidaan iteroida

## Käyttö

Julia-komentoriviohjelman voi ladata kielen virallisilta web-sivuilta valmiina ajotiedostona ohjeiden mukaan. Kieli voidaan asentaa myös juliaup-työkalulla, joka helpottaa myös kielen päivittämistä ja useiden eri versioiden rinnakkaista käyttöä.

`julia --help` näyttää saatavilla olevat asetukset ja `julia` käynnistää REPL-konsolin. Komentoriviä voidaan käyttää poistumatta REPL-tilasta `;`-komennolla ja paketinhallintatilaan päästään `]`-komennolla. `]?` näyttää kaikki Pkg-pakettienhallinnan REPL-tilan komennot, jotka vastaavat Pkg-paketin funktioita. REPL-ohjelmasta poistutaan `<CTRL>-D`. Julia-kääntäjää käytetään yhdelle lähdetiedostolle yksinkertaisesti `julia hei-wikipedia.jl`. (Tämä Julia-kääntäjä toimii ns. JAOT-periaatteella (engl. *just-ahead-of-time*) eli Julia-koodi käännetään tyypitetyn ja LLVM-välikoodin kautta konekielelle, mutta tämä tapahtuu ajon aikana eikä erillisessä käännösvaiheessa.)

Julia REPL mahdollistaa interaktiivisemman ohjelmointitavan, missä kehitettävät ohjelmat ovat omassa moduulissaan ja tiedostossaan, testit ovat omassa moduulissaan ja tiedostossaan, testit ajetaan Julia-konsolissa tyylillä `include("testit.jl")` ja tämän lisäksi tehdään interaktiivista testaamista konsolissa. Kun interaktiivisesti löydetään jotain uutta testaamisen arvoista, tämä voidaan lisätä testimoduuliin. Muuten kehityksen aikana tehdään moduuleihin muutoksia palautteen mukaan ja toistetaan kehityssilmukkaa. `Revise`-paketti mahdollistaa päivitettyjen ohjelmien testaamisen käynnistämättä Juliaa uudelleen.

Pluto.jl on Julialla Julialle kehitetty, Jupyter-työkirjojen kaltainen web-selaimeen ja ajettaviin soluihin perustuva kehitysympäristö, joka mahdollistaa interaktiivisen kirjallisen ohjelmoinnin (engl. *literate programming*). Työkirjat ovat aina toistettavia, sillä Pluto automatisoi pakettienhallinnan `import`- ja `using`-lauseiden perusteella.

## Esimerkki

Fibonaccin luvut.

```julia
function fibonacci(n)
  f = [0, 1]
  # Julia-listojen indeksit alkavat ykkösestä.
  n in [0, 1] && return f[n + 1]
  # Ajon fibonacci(n = 2) tulos tulee listan indeksiin 3, jne. 
  for i = 3:(n + 1)
    # Huutomerkkiin päättyvät funktiot muokkaavat parametrejaan.
    append!(f, f[i - 1] + f[i - 2])
  end
  # end-sana viittaa listan vimeiseen indeksiin.
  return f[end]
end
```


## Rakenne

### Modularisointi

Julian `Pkg`-paketti mahdollistaa vakioitujen ympäristöjen luomisen, pääosin `activate` ja `add`/`rm` funktioidensa avulla. Tietty pakettiversio voidaan vaatia syntaksilla `Pkg.add("Nimi@1.3")` (semanttinen versiointi) tai `Pkg.add("Nimi#master")` (Git). Myös suora URL toimii, jos kyseinen paketti ei ole käytetyssä rekisterissä. Nämä komennot muokkaavat kahta projektikansion juuressa olevaa tiedostoa: `Project.toml` (metadata) ja `Manifest.toml` (riippuvuudet). `Pkg.instantiate` asentaa halutun Julia-ympäristön uudelleen näiden tiedostojen pohjalta. Julia-koodin lisäksi muiden riippuvuuksien käsittelemiseksi voidaan käyttää `Artifacts.toml`-tiedostoa, jonka avulla kunkin riippuvuuden paikallinen tiedostopolku saadaan käyttöön projektin Julia-koodissa syntaksilla `polku = artifact"nimi"` (engl. *non-standard string literal*).

Kun ympäristö koostuu useista paketeista, niin paketti muodostuu yleensä useiden moduulien hierarkiasta. Moduulit `module Nimet ... end` luovat uuden globaalin nimiavaruuden, johon tuodaan nimiä `import` ja `using` lauseilla ja lähdetiedostoja `include()`-kutsulla ja josta paljastetaan nimiä `export`-lauseilla (vain `using` ottaa `export`-lauseet huomioon). Paikalliseen moduuliin viittaamiseksi tarvitaan pistesyntaksia `using .Nimi`, koska pisteetön syntaksi viittaa Julia-paketteihin.

Juliassa on moduulin luoma globaali näkymä ja tämän sisällä ns. heikkoja ja vahvoja paikallisia näkymiä. `if..else` ja `begin` lauseet eivät luo ollenkaan uutta näkymää, kun taas `struct`, `for`, `while` ja `try` lauseet luovat paikallisen näkymän, jossa globaalien nimien uudelleenkäyttö on kielletty (heikko näkymä). Funktiot ja makrot luovat vahvan paikallisen näkymän eli ulkonäkymien nimiin sijoittaminen on sallittua eikä muokkaa niitä. Kullakin paikallisella näkymällä on kuitenkin lukuoikeus sen ulkonäkymissä määriteltyihin muuttujiin. Esim. funktiot perivät arvot määrittelympäristöstään, eli esim. tietyn moduulin funktiot voivat lukea samoja globaaleja muuttujia moduulistaan ilman erityisiä avainsanoja.

### Tyypit

Julian tyyppijärjestelmä on dynaaminen, nominatiivinen, parametrinen ja yhtenäinen. Kaikilla rakenteilla on jokin tyyppi ajon aikana. 

Kielen mukana tulevia tietotyyppejä (`typeof()`) ovat muun muassa seuraavat:
* `AbstractString`, `AbstractChar` ja näiden Unicode-UTF-8-alityypit `String` ja `Char`. `Substring{String}` edustaa tekstiviipaletta. Unicode UTF-8 sisältää joitakin monitavuisia merkkejä, joten turvalliseen indeksointiin voidaan käyttää `nextind()`-, `prevind()`- tai `eachindex()`-funktioita. 
* `Number` on kaikkien numeroiden (`Int64`, `Real`) ylätyyppi.
* `Missing` ja tämän ainoa arvo `missing`. Numeerisissa ohjelmissa `missing` tekee tuloksesta epävarman, joten odotettu tulos on yleensä `missing`. Tämä ei pidä välttämättä paikkaansa muissa tilanteissa, jolloin voidaan käyttää esim. `Missings.passmissing` tai `Base.skipmissing` funktioita. Puuttuvia sisältävälle kokoelmalle voidaan kutsua myös `collect()`.
* Kaikki funktiot ovat omaa tyyppiänsä ja `Function`-tyypin alatyyppejä. Funktioita voidaan syöttää funktioihin, käsitellä funktioissa ja palauttaa funktioista muiden arvotyyppien tapaan.

Uusi yhdistelmätyyppi luodaan sanoilla `struct Nimi ... end`. Instansseja voidaan muokata vain, kun on käytetty `mutable struct Nimi ... end`, ellei tyypin alkiota olla määritelty sanalla `const`. Tyyppejä voidaan yhdistää myös tyylillä `MaybeInt = Union{Int, Nothing}`. Tyyppiparametreja käytetään syntaksilla `struct Tyyppi{T} ... end`, mikä määrittelee kerralla joukon tyyppejä. Parametrina olevien tyyppien joukko voidaan rajoittaa alatyypeihin tyylillä `Tyyppi{T<:YlinTyyppi}`. Tyyppi voidaan myös parametrisoida vain osittain syntaksilla `Array{T,1} where T`. Abstrakteja tyyppejä `abstract type Tyyppi <: Supertyyppi end` taas ei voida käyttää suoraan vaan ne täytyy ensin konkretisoida. Tämän jälkeen abstraktia tyyppiä voidaan käyttää viittaamaan kaikkiin tämän alatyyppeihin. (Juliassa ei ole olioperusteisemmille kielille tyypillistä perimismekanismia.)

Luodut tyyppiobjektit toimivat itse alustajinaan `struct Tyyppi ... end; t = Tyyppi(...)`. Tämä alustaja on tavallinen funktio, joten sille voidaan määritellä useita metodeja `Tyyppi(...) = ...`. Tyypin alustajaa voidaan käyttää myös tyypin omassa määritelmässä erityisen `new()`-funktion kautta. Alustettujen arvojen tyyppiä voidaan selvissä tapauksissa muuttaa `convert(Tyyppi, arvo)`- tai `promote(arvo, arvo)`-funktiolla. (Numeroita edustaville `String`-arvoille (`"2.54"`) löytyy oma `parse()`-funktionsa.) 

Tietylle tyypille määritellään metodeja Juliassa tavallisten funktioiden tapaan `function f(x::Tyyppi) ... end`. Metodi ei kuitenkaan kuulu ainoastaan kyseiselle tyypille vaan kutsuttava metodi valitaan koko funktion tyyppijäljen perusteella (engl. *multiple dispatch*). Funktionkin tyyppijälki voidaan parametrisoida tyylillä `funktio(x::T, y::T, z::T) where {T} ... end` (eli tämä metodi valitaan, kun kolmen argumentin tyypit ovat samoja). Näin myös objekteista itsestään voidaan tehdä kutsuttavia (ns. funktoreita) antamalla niiden *tyypille* metodi tyylillä `function (x::Tyyppi)() ... end`. Olemassaolevien rajapintojen toteuttaminen tapahtuu toteuttamalla tarvittavien funktioiden metodit – uudesta tyypistä voidaan tehdä muun muassa iteroitava, indeksoitava ja listamainen määrittelemällä sille metodit muutamista erityisistä funktioista kuten `iterate`, `getindex` ja `size`.

### Funktiot

Funktiot määritellään `function nimi(x) ... end` tai yhdellä rivillä `nimi(x) = ...`. Nimettömille funktioille voidaan käyttää syntaksia `(x, y) -> ...` tai `function (x) ... end` tai `x -> begin ... end`. `funktio(y) do x ... end` tekee anonyymin funktion `do`-lauseen avulla ja syöttää sen edellä olevan funktion ensimmäiseksi argumentiksi. Funktion voi ns. vektorisoida eli kutsua jonkin kokoelman jokaiselle alkiolle käyttämällä pistesyntaksia kuten `x .= sin.(y)` (tai sama pistemakrolla `@. x = sin(y)`, kun kaikki operaatiot vektorisoidaan). Funktioiden yhdistäminen on mahdollista muun muassa putkioperaattorilla `x = 1:3 .|> sum |> sqrt`.

Funktioiden parametrit ovat Juliassa uusia muuttujia, mutta ne vain viittaavat annettuun arvoon eli kopioita ei synny. Funktiot voidaan tyypittää tyylillä `function funktio(x::Tyyppi=0, args...; y::Tyyppi="oletus", kwargs...)::Tyyppi end`, mutta yleensä Julian tyypinpäättelyn vuoksi korkeintaan parametrien tyyppien merkitseminen on tarpeen. Funktio voi hyväksyä vaihtelevan määrän parametreja tyylillä `funktio(x,y,z...)`, missä `z` on `Tuple` funktion sisällä tai mikä tahansa iteroitava tyyppi funktion ulkona. Funktioiden tulisi palauttaa vain yhden tyypin arvoja, jolloin tulostyyppi on Julia-kääntäjän pääteltävissä. Tyyppien merkitseminen voi kuitenkin parantaa koodin luettavuutta. Destrukturointi onnistuu taas tyylillä `a, b, _ = iteraattori(x)`. 

Sivuvaikutusten takia kutsutut funktiot palauttavat käytännön mukaan `nothing`. Parametrejaan muuttavien funktioiden nimi päättyy huutomerkkiin `teejotain!`. Funktioiden nimeämiskäytäntö on ilman alaviivoja kuten `teenytjotain()`, kun tämä vain on luettavissa – muussa tapauksessa sanat erotetaan alaviivalla. 

Juliassa myös kaikki operaattorit ovat funktioita: esim. `x[i]` kutsuu `getindex`, `[1 2 3]` kutsuu `hcat` ja `x.nimi = y` kutsuu `setproperty!`.

### Metaohjelmointi

Metaohjelmointi onnistuu suoraan käsittelemällä Julia-ohjelmien abstraktia syntaksipuuta. Tämä mahdollistaa täyden metaohjelmoinnin. Julia-koodin tyyppi on aluksi tyyppiä `String`, joka voidaan jäsentää `Meta.parse(koodi)` lauseketyypin `Expr` arvoksi. Lausekkeita voidaan määritellä myös syntaksilla `:( 1 + 2 + $numero )` ja usean rivin lausekkeita syntaksilla `quote ... end`. Kaksoispisteellä tehdään myös `Symbol`-tyypin arvoja `:nimi` (engl. *string interning*). Lausekkeet voidaan lopuksi ajaa `eval`-funktiolla. Lausekkeita voidaan siis käsitellä normaaleissa Julia-funktioissa, mutta nämä funktiot ajetaan vasta ajon aikana, kun taas metaohjelmoinnissa on yleensä tehokkaampaa tuottaa halutut lausekkeet jo käännösvaiheessa. 

Käännösaikaiseen metaohjelmointiin tarvitaan makroja. Makrot sijoittavat argumenttinsa suoraan tuloksena tuotettuun lausekkeeseen, joka käännetään sitten normaalin Julia-koodin tapaan. Makrot määritellään tyylillä `macro nimi() ... end` ja kutsutaan syntaksilla `@ajamakro x y` tai `@ajamakro(x, y)` (tai yhdelle literaalitaulukolle `@ajamakro[1, 2, 3]`). Makro saa argumenttinsa vain lausekkeina `Expr`, symboleina `:nimi` tai literaaleina. Koska Julia on dynaamisesti tyypittävä, makrot eivät tiedä saamiensa arvojen tyyppejä. Makrot noudattelevat muuten paljolti samoja sääntöjä kuin funktiot.

Normaalin funktion ja makron lisäksi voidaan määritellä myös ns. tuotettu funktio tyylillä `@generated function nimi() ... end`, joka asettuu ominaisuuksiltaan funktion ja makron välille. Tällainen funktio ajetaan vasta, kun argumenttien tyypit tiedetään, mutta ennen kuin funktio on käännetty. Makrojen tapaan nämä erityisfunktiot lisäävät abstraktiokykyä ja suorityskykyä siirtämällä laskentaa ajovaiheesta käännösvaiheeseen.

### Rinnakkaisohjelmointi

`@async`-makro tekee annetusta lausekkeesta asynkronisesti ajettavan `Task`-tyypin objektin, jolle voidaan kutsua `schedule()`, `wait()` ja `fetch()`. Vastaavasti `@sync`-makron avulla voidaan odottaa kaikkien makron sisältämien `@async`-lausekkeiden valmistumista. `Task`-objekteja voidaan luoda myös käyttämällä `@task`-makroa tai tyypin alustajaa `Task(() -> x)`. `Channel` on taas ikäänkuin rinnakkainen versio generaattorista; kanavan tuottaja kutsuu `put`-funktiota ja kuluttaja `take`-funktiota tai iteroi kanavaa muiden iteroitavien tyyppien mukaan. 

Julia käyttää oletuksena yhtä säiettä (`julia --threads=1`). Julia ei myöskään varmista muistiturvallisuutta (vrt. Rust (ohjelmointikieli)) vaan tämä on ohjelmoijan vastuulla. Esim. datakisavirheiden ehkäisemiseksi tulee käyttää lukkomekanismia, kuten `ReentrantLock()`, `lock()` ja `unlock()`, tai ns. atomisia arvoja, kuten `Threads.Atomic{Tyyppi}(arvo)` ja `Threads.atomic_add!()`. `Threads`-paketti määrittelee helppokäyttöisen `@threads`-makron, joka tekee annetusta silmukkalausekkeesta automaattisesti monisäikeisen.

`julia -p x` tekee paikallisen klusterin, jossa on annettu määrä prosesseja, ja `julia --machine-file f` tekee usean koneen klusterin käyttämällä salasanatonta ssh-viestintää näiden välillä. `Distributed`-paketti mahdollistaa hajautetun laskennan useissa prosesseissa ja perustuu etäreferensseihin (`Future` ja `RemoteChannel`) ja etäkutsuihin (`@spawnat :any ...`, `remotecall()`). `Future`-tyypille kutsutaan lopulta `fetch()` tuloksen saamiseksi pääprosessiin. Lähdetiedosto voidaan lisätä jokaiseen prosessiin tyylillä `@everywhere include("Nimi.jl")` tai tekemällä tästä Julia-paketti. Prosesseja voidaan lisätä, poistaa ja muokata funktioilla kuten `addprocs` ja `rmprocs`. Esimerkiksi `DistributedArrays`-paketin `DArray` on taulukon hajautettu versio ja standardikirjaston `SharedArray` mahdollistaa hajautettujen osasten jakamisen useiden prosessien välillä. 

Grafiikkaprosessoreiden käyttöön löytyy tukea muun muassa paketeista CUDA.jl ja MPI.jl.

### Muut

Dokumentointi onnistuu Juliassa tekstiliteraaleilla kohteena olevan määritelmän yläpuolella (engl. *docstring*). Teksti voi sisältää Markdown- ja LateX-merkintöjä.

Julian tärkeimpiin siirrännän työkaluihin kuuluvat ensinnäkin `stdin`- ja `stdout`-virrat, jotka tukevat `print`, `show`, `read` ja `write` funktioita. `open`-funktio tekee tiedostonimestä `IOStream`-objektin ja `close`-sulkee virran, jos `open` kutsuttiin ilman funktioargumenttia. `Sockets`-paketti määrittelee TCP-viestintään pääfunktiot `listen`, `accept`, `connect` ja `close`.

## Lähteet

## Aiheesta muualla

* Julia-kielen kotisivu 
* Julia-kielen lähdekoodi