概要
Juliaという言語を知っているだろうか?Cの速度、Pythonの使い勝手など、様々な言語の長所を取り入れようとする野心的な言語だ1。
最近はMojoが話題2だけれど、逆張りでJuliaを勉強したから、印象に残った点をPythonと比較しつつまとめてみる。これからJuliaを勉強するPython使いには、何かしら参考になるかもしれない。網羅的な説明ではないので、Noteworthy Differences from Pythonも合わせて読むべし。
また、実行速度やメモリの効率とか、そういう話も触れない。
開発体験っぽい話
バージョン管理
Juliaのバージョン管理にはjuliaupというツールを使える。Pythonでいうpyenvのような位置づけだが、このようなツールが公式から提供されているのは安心感がある。
やや気になったのは、ディレクトリごとにjuliaのバージョン3を指定する方法(pyenvでいうpyenv local
)が提供されていないこと4。defaultではないjuliaのバージョンを実行する場合はjulia +1.9.0
のようにする必要がある。
# 特定の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
パッケージ管理
Pythonのパッケージ管理ツールはPoetry・Pipenvなど乱立しているが、JuliaだとデフォルトのPkg.jlのみで最低限のことはできそう。pkgモード(]
を押した状態)にて、以下のように特定のprojectに特定のpackageを追加できる。かなり直感的。
generate HelloWorld # HelloWorldという新規projectの作成
activate HelloWorld # HelloWorldというprojectを有効化
add Example # 有効化したHelloWorldというprojectに、Exampleという依存パッケージを追加
依存パッケージは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も管理できる。ただ、同じことをやる方法がもう一つあるらしく5、やや混乱気味に見える。
[extras]
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
[targets]
test = ["Test"]
パッケージ公開
Juliaで自作パッケージを公開するにはGeneralというregistryに登録する必要がある(PythonでいうPyPIだと思ってほしい)。用意されたGitHub Appを利用すると、GeneralのGitHub reposiotryにPull Requestが作成され、merge後に公開される仕組み。
特に難しいことはないが、パッケージ名などはCIでチェックされるのでルールを守らないと以下のようにはじかれる。事前にguidelineをチェックだ!
言語仕様っぽい話
型定義
例えばBookという型と、そのサブタイプとしてPaperBook・EBookという型を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で似たことをするならこんな感じか?
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と呼ぶらしい(引用元)。以下に例を示す。
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倍して加えられている)ことがわかるだろう6。円安は気にしない方向で。
Yen(100) + Yen(1) # Yen(101)
Yen(100) + Dollar(1) # Yen(200)
Pythonならこう書くことになるかと7。
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
Yen(100) + Yen(1) # Yen(101)
Yen(100) + Dollar(1) # Yen(200)
実行時型チェック
ちょっと筆者の理解が浅かったので、コメント欄の議論も見てもらえればと
Juliaで以下のようなコードは書けない。
i::Integer = 1
i = "one" # ERROR: MethodError: Cannot `convert` an object of type String to an object of type Integer
一方でPythonだと特にエラーは生じない。こちらはあくまでtype hintだから8。
i: int = 1
i = "one"
配列のIndex
Juliaだと配列のIndexが1から始まる。慣れないと気持ち悪いだろうが、数値計算を意識した言語ではままある話なので、Juliaだけ特殊なわけではない9。
# julia
["a", "b", "c"][1] # "a"
数値型
Pythonにデフォルトで組み込まれた数値型はint
float
complex
の3種類(Built-in Types)。一方で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)10を使うことが多いと思うが、先日ファミコンのエミュレータ(FamilyComputer.jl)を作った際はUInt8とUInt16の存在に助けられた。
pipe
Juliaには|>
という演算子がある。これは引数を一つとる関数をつなげて、連続して処理する演算子。endswith("O")
って関数なのかって思うかもしれないが、これは関数を返す。
"foo" |> uppercase |> endswith("O") # true
Pythonだと普通は以下のように書くと思う11。この程度なら読みやすさは変わらない。
"foo".upper().endswith("O") # True
ただ、filterやmapが必要になるとJuliaの|>
の恩恵を感じることがあるかもしれない。
# Julia
upper(xs) = map(x -> uppercase(x), xs)
["foo", "bar"] |> upper |> filter(endswith("O")) # ["Foo"]
# 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のmacroで書けたりする12。
# Python
data = 1
match data:
case 1:
print("one!")
case 2:
print("two!")
case _:
print("no match!")
# Julia
using MLStyle
data = 1
@match data begin
1 => print("one!")
2 => print("two!")
_ => print("no match!")
end
macroって便利だけれど、ものによってはLanguage Serverがうまく機能しなかったりちょっと辛みも感じた13。
マルチバイト文字の入力
Juliaで整数除算を行う演算子は÷
である(Pythonだと//
)。「こんな文字打てるか!」と思ったけれど、REPL上は\div<tab>
で入力できるようになっている。演算子以外にギリシャ文字なども\
から入力できるようになっていて驚き。🙏も\:pray:<tab>
で入力できるよ!
最後に
たまに新しい言語を書くと勉強になるから、食わず嫌いせずにいろいろ触っていきたい。
-
Why We Created Juliaをぜひ読んでほしい。 ↩
-
ちなみにMojoのドキュメントでJuliaについての言及がある。これによると、Pythonとの親和性や後発言語ならではの優位性といった点で、MojoはJuliaと別物とのこと。 ↩
-
READMEの用語に揃えてchannelと呼んだ方が適切かもしれない。 ↩
-
詳細はTest specific dependenciesを読んでね。 ↩
-
どちらが第一・第二引数かを強調したければ
+(Yen(100) + Yen(1))
などと書くこともできる。 ↩ -
multipledispatchというパッケージを見つけたから、これを使うのもありかも。 ↩
-
細かいことはIntegers and Floating-Point Numbersを読んでほしい。 ↩