Juliaを学んで驚いたこと ~Pythonとの比較を添えて~

## 概要
[Julia](https://julialang.org/)という言語を知っているだろうか？Cの速度、Pythonの使い勝手など、様々な言語の長所を取り入れようとする野心的な言語だ[^why]。

最近は[Mojo](https://www.modular.com/mojo)が話題[^mojo]だけれど、逆張りでJuliaを勉強したから、印象に残った点をPythonと比較しつつまとめてみる。これからJuliaを勉強するPython使いには、何かしら参考になるかもしれない。網羅的な説明ではないので、[Noteworthy Differences from Python](https://docs.julialang.org/en/v1/manual/noteworthy-differences/#Noteworthy-differences-from-Python)も合わせて読むべし。

[^why]: [Why We Created Julia](https://julialang.org/blog/2012/02/why-we-created-julia/)をぜひ読んでほしい。
[^mojo]: ちなみにMojoのドキュメントで[Juliaについての言及](https://docs.modular.com/mojo/faq.html#why-not-make-julia-better)がある。これによると、Pythonとの親和性や後発言語ならではの優位性といった点で、MojoはJuliaと別物とのこと。

また、実行速度やメモリの効率とか、そういう話も触れない。

## 開発体験っぽい話
### バージョン管理

Juliaのバージョン管理には[juliaup](https://github.com/JuliaLang/juliaup)というツールを使える。Pythonでいう[pyenv](https://github.com/pyenv/pyenv)のような位置づけだが、このようなツールが公式から提供されているのは安心感がある。

やや気になったのは、ディレクトリごとにjuliaのバージョン[^channel]を指定する方法（pyenvでいう`pyenv local`）が提供されていないこと[^local]。defaultではないjuliaのバージョンを実行する場合は`julia +1.9.0`のようにする必要がある。

[^channel]: READMEの用語に揃えてchannelと呼んだ方が適切かもしれない。

```sh
# 特定のversionを追加
juliaup add 1.9.0 # Installing Julia 1.9.0+0.x64.linux.gnu

# versionを指定してjuliaを実行
julia +1.9.0 --version # julia version 1.9.0
```

[^local]: 関連[Issue](https://github.com/JuliaLang/juliaup/issues/180)。

### パッケージ管理

Pythonのパッケージ管理ツールは[Poetry](https://python-poetry.org/)・[Pipenv](https://pipenv.pypa.io/en/latest/)など乱立しているが、Juliaだとデフォルトの[Pkg.jl](https://pkgdocs.julialang.org/v1/)のみで最低限のことはできそう。pkgモード（`]`を押した状態）にて、以下のように特定のprojectに特定のpackageを追加できる。かなり直感的。

```julia
generate HelloWorld # HelloWorldという新規projectの作成
activate HelloWorld # HelloWorldというprojectを有効化
add Example # 有効化したHelloWorldというprojectに、Exampleという依存パッケージを追加
```

[^rye]: 最近では[Rye](https://github.com/mitsuhiko/rye)というツールの名前も聞く。

依存パッケージはProject.tomlに記録される。

```toml:Project.toml
name = "HelloWorld"
uuid = "ed5aed9b-a0a7-41f7-9bdc-cc2f4f65765a"
authors = ["xxxxx <xxxxx@example.com>"]
version = "0.1.0"

[deps]
Example = "7876af07-990d-54b4-ab0e-23690620f79a"
```

以下のように`[extras]`と`[targets]`に追記することで、テストのみで依存するpackageも管理できる。ただ、同じことをやる方法がもう一つあるらしく[^test]、やや混乱気味に見える。

```toml:Project.toml
[extras]
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[targets]
test = ["Test"]
```

[^test]: 詳細は[Test specific dependencies](https://pkgdocs.julialang.org/dev/creating-packages/#Test-specific-dependencies)を読んでね。

### パッケージ公開

Juliaで自作パッケージを公開するには[General](https://github.com/JuliaRegistries/General)というregistryに登録する必要がある（Pythonでいう[PyPI](https://pypi.org/)だと思ってほしい）。用意された[GitHub App](https://github.com/JuliaRegistries/Registrator.jl#via-the-github-app)を利用すると、GeneralのGitHub reposiotryにPull Requestが作成され、merge後に公開される仕組み。

特に難しいことはないが、パッケージ名などはCIでチェックされるのでルールを守らないと以下のようにはじかれる。事前に[guideline](https://pkgdocs.julialang.org/dev/creating-packages/#Package-naming-guidelines-1)をチェックだ！

https://github.com/JuliaRegistries/General/pull/90719

## 言語仕様っぽい話
### 型定義
例えばBookという型と、そのサブタイプとしてPaperBook・EBookという型をJuliaで定義すると以下のようになる。

```julia
abstract type Book end

struct PaperBook <: Book
    title::String
    author::String
    page::Integer
end

struct EBook <: Book
    title::String
    author::String
    file_size::Integer
end

function Base.show(io::IO, book::Book)
    print(io, book.title, " / ", book.author)
end
```

一方でPythonで似たことをするならこんな感じか？

```py
from dataclasses import dataclass

@dataclass(frozen=True)
class Book:
    title: str
    author: str

    def __str__(self):
        return f"{self.title} / {self.author}"

@dataclass(frozen=True)
class PaperBook(Book):
    page: int

@dataclass(frozen=True)
class EBook(Book):
    file_size: int
```

以下、注目してほしい点。

* JuliaのBookは抽象型として定義する必要がある（具体型のサブタイプを定義することはできないという制約がある）
* Juliaの抽象型ではfieldを定義できないため、共通のfieldであるtitle・authorも、PaperBook・EBookそれぞれで定義している（DRYに書けると嬉しいのだが）
* `Base.print`と`__str__`methodの位置の違い（Pythonでは型定義の中でmethodを定義するが、Juliaでは型定義とは別に定義する）
* Juliaの型はデフォルトでimmutable

### multiple dispatch

>  Using all of a function's arguments to choose which method should be invoked, rather than just the first, is known as multiple dispatch.

Juliaでは関数に渡された全ての引数を考慮して、実行するmethodを決定する。この機能のことをmultiple dispatchと呼ぶらしい（[引用元](https://docs.julialang.org/en/v1/manual/methods/#Methods)）。以下に例を示す。

```julia
import Base:+

struct Yen
    yens::Integer
end

struct Dollar
    dollars::Integer
end

function +(yen1::Yen, yen2::Yen)
    Yen(yen1.yens + yen2.yens)
end

function +(yen::Yen, dollar::Dollar)
    Yen(yen.yens + dollar.dollars * 100)
end
```

`+`というmethodが二度定義されているが、どちらを実行するべきかは第二引数の型を確認すれば判断できそうだ。実際に実行すると、Dollarが第二引数の場合は二つ目のmethodが実行されている（100倍して加えられている）ことがわかるだろう[^infix]。円安は気にしない方向で。

[^infix]: どちらが第一・第二引数かを強調したければ`+(Yen(100) + Yen(1))`などと書くこともできる。

```julia
Yen(100) + Yen(1) # Yen(101)
Yen(100) + Dollar(1) # Yen(200)
```

Pythonならこう書くことになるかと[^multipledispatch]。

[^multipledispatch]: [multipledispatch](https://github.com/mrocklin/multipledispatch)というパッケージを見つけたから、これを使うのもありかも。

```py
class Yen:
    def __init__(self, yens):
        self.yens = yens

    def __add__(self, other):
        if isinstance(other, Yen):
            return Yen(self.yens + other.yens)
        elif isinstance(other, Dollar):
            return Yen(self.yens + other.dollars * 100)
        else:
            raise Exception()

class Dollar:
    def __init__(self, dollars):
        self.dollars = dollars
```

```py
Yen(100) + Yen(1) # Yen(101)
Yen(100) + Dollar(1) # Yen(200)
```

### 実行時型チェック

:::note warn
ちょっと筆者の理解が浅かったので、コメント欄の議論も見てもらえればと :pray:
:::

Juliaで以下のようなコードは書けない。

```julia
i::Integer = 1
i = "one" # ERROR: MethodError: Cannot `convert` an object of type String to an object of type Integer
```

一方でPythonだと特にエラーは生じない。こちらはあくまでtype hintだから[^pydantic]。

[^pydantic]: Pythonでも[Pydantic](https://docs.pydantic.dev/latest/)とかで実行時型チェックしようという風潮はあるよね。

```py
i: int = 1
i = "one"
```

### 配列のIndex

Juliaだと配列のIndexが1から始まる。慣れないと気持ち悪いだろうが、数値計算を意識した言語ではままある話なので、Juliaだけ特殊なわけではない[^onebased]。

[^onebased]: 関連[議論](https://discourse.julialang.org/t/whats-the-big-deal-0-vs-1-based-indexing/1102/5)。

```julia
# julia
["a", "b", "c"][1] # "a"
```

### 数値型

Pythonにデフォルトで組み込まれた数値型は`int` `float` `complex`の3種類（[Built-in Types](https://docs.python.org/3/library/stdtypes.html#numeric-types-int-float-complex)）。一方でJuliaの場合、以下の通り利用するbit数ごとに細かく定義されている。

```
Number
├─ Complex
└─ Real
   ├─ AbstractFloat
   │  ├─ BigFloat
   │  ├─ Float16
   │  ├─ Float32
   │  └─ Float64
   ├─ AbstractIrrational
   │  └─ Irrational
   ├─ Integer
   │  ├─ Bool
   │  ├─ Signed
   │  │  ├─ BigInt
   │  │  ├─ Int128
   │  │  ├─ Int16
   │  │  ├─ Int32
   │  │  ├─ Int64
   │  │  └─ Int8
   │  └─ Unsigned
   │     ├─ UInt128
   │     ├─ UInt16
   │     ├─ UInt32
   │     ├─ UInt64
   │     └─ UInt8
   └─ Rational
```

例えば整数だと無意識でデフォルトのInt64（ないしInt32）[^default]を使うことが多いと思うが、先日ファミコンのエミュレータ（[FamilyComputer.jl](
https://github.com/kitta65/FamilyComputer.jl)）を作った際はUInt8とUInt16の存在に助けられた。

[^default]: 細かいことは[Integers and Floating-Point Numbers](https://docs.julialang.org/en/v1/manual/integers-and-floating-point-numbers/#Integers)を読んでほしい。

### pipe

Juliaには`|>`という演算子がある。これは引数を一つとる関数をつなげて、連続して処理する演算子。`endswith("O")`って関数なのかって思うかもしれないが、これは関数を返す。

```julia
"foo" |> uppercase |> endswith("O") # true
```

Pythonだと普通は以下のように書くと思う[^pipe]。この程度なら読みやすさは変わらない。

```py
"foo".upper().endswith("O") # True
```

[^pipe]: [Pipe](https://github.com/JulienPalard/Pipe)というpackageを見つけたから、これを使うこともできるかもしれない。

ただ、filterやmapが必要になるとJuliaの`|>`の恩恵を感じることがあるかもしれない。

```julia
# Julia
upper(xs) = map(x -> uppercase(x), xs)

["foo", "bar"] |> upper |> filter(endswith("O")) # ["Foo"]
```

```python
# Python
res = filter(
    lambda x: x.endswith("O"),
    map(lambda x: x.upper(), ["foo", "bar"]),
)
list(res) # ["Foo"]
```

### macro

Juliaはmacroが書ける。Pythonで`@foo`と書けばdecoratorだが、Juliaの文脈ではmacroの呼び出しだ。macroを使えば様々なことができるが、例えばPythonのmatch文のような処理を[MLStyle.jl](https://thautwarm.github.io/MLStyle.jl/latest/index.html)のmacroで書けたりする[^match]。

[^match]: macroを使わなくても表現できるようにしようという[議論]((https://github.com/JuliaLang/julia/issues/18285))はあるが、合意に至っていなそう。

```python
# Python
data = 1

match data:
    case 1:
        print("one!")
    case 2:
        print("two!")
    case _:
        print("no match!")
```

```julia
# Julia
using MLStyle

data = 1

@match data begin
    1 => print("one!")
    2 => print("two!")
    _ => print("no match!")
end
```

macroって便利だけれど、ものによってはLanguage Serverがうまく機能しなかったりちょっと辛みも感じた[^ls]。

[^ls]: 自分は勉強中に[このあたり](https://github.com/julia-vscode/LanguageServer.jl/issues/1161)の制約で困った。

### マルチバイト文字の入力

Juliaで整数除算を行う演算子は`÷`である（Pythonだと`//`）。「こんな文字打てるか！」と思ったけれど、REPL上は`\div<tab>`で入力できるようになっている。演算子以外にギリシャ文字なども`\`から入力できるようになっていて驚き。🙏も`\:pray:<tab>`で入力できるよ！


## 最後に

たまに新しい言語を書くと勉強になるから、食わず嫌いせずにいろいろ触っていきたい。