LoginSignup
8
6

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

Last updated at Posted at 2023-11-05

概要

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のパッケージ管理ツールはPoetryPipenvなど乱立しているが、JuliaだとデフォルトのPkg.jlのみで最低限のことはできそう。pkgモード(]を押した状態)にて、以下のように特定のprojectに特定のpackageを追加できる。かなり直感的。

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

依存パッケージはProject.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も管理できる。ただ、同じことをやる方法がもう一つあるらしく5、やや混乱気味に見える。

Project.toml
[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)

実行時型チェック

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

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>で入力できるよ!

最後に

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

  1. Why We Created Juliaをぜひ読んでほしい。

  2. ちなみにMojoのドキュメントでJuliaについての言及がある。これによると、Pythonとの親和性や後発言語ならではの優位性といった点で、MojoはJuliaと別物とのこと。

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

  4. 関連Issue

  5. 詳細はTest specific dependenciesを読んでね。

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

  7. multipledispatchというパッケージを見つけたから、これを使うのもありかも。

  8. PythonでもPydanticとかで実行時型チェックしようという風潮はあるよね。

  9. 関連議論

  10. 細かいことはIntegers and Floating-Point Numbersを読んでほしい。

  11. Pipeというpackageを見つけたから、これを使うこともできるかもしれない。

  12. macroを使わなくても表現できるようにしようという議論はあるが、合意に至っていなそう。

  13. 自分は勉強中にこのあたりの制約で困った。

8
6
7

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
6