6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

RedMonk Programming Language Rankings: Jan 2024 1を眺めていて、ZigMoonbit 等と並んで Grain が言及されていることに気が付きました。2

lang.rank_.q124.wm_-2048x1689

Bicep (86), Grain, Moonbit, Zig (97): as with the Dart/Kotlin/Rust grouping above, these languages are grouped here not because they’re all technically similar but rather because they are on the languages of interest list mentioned above. They are included here for a variety of reasons: Zig is on here because it has attempted to learn from the languages that preceded it from C++ to Rust. Grain and Moonbit are on here, meanwhile, because they are optimized for WebAssembly. Bicep is on here because it comes up with surprising frequency – and a mildly surprising ranking – for a cloud DSL. Only two of these languages are currently ranked, but we’re watching all of them to see if these or any other new languages begin to emerge.

Bicep (86)、Grain、Moonbit、Zig (97):上記の Dart/Kotlin/Rust のグループ分けと同様に、これらの言語がここにグループ分けされているのは、技術的にすべて似ているからではなく、むしろ上記の興味のある言語リストに含まれているからだ。これらはさまざまな理由でここに含まれている: Zig は、C++ から Rust まで、先行する言語から学ぼうとしている。一方、Grain と Moonbit は WebAssembly に最適化されているため、ここに含まれている。Bicep は、クラウド DSL としては意外な頻度で、そして少し意外な順位でランクインしている。現在ランクインしているのはこれらの言語のうち2つだけだが、これらの言語やその他の新しい言語が出現し始めるかどうか、すべての言語をウォッチしている。(DeepL 訳を元に修正)

Grain は WebAssembly ファーストな関数型プログラミング言語です。OCaml に影響を受けているというか、Grain 自体が Reason というOCaml な AltJS3で書かれています。

Grain の作者である Oscar Spencer 氏が WasmCon に登壇されていた記事等で認識は持っていましたが、Qiita や Zenn のようなコミュニティで記事を見た覚えはありません。せっかくなので、ここに触ってみた情報を記録することにします。

導入と開発環境の構築

公式の Getting Grain を参照します。といっても、Grain ツールチェイン (CLI、コンパイラ、ランタイム、標準ライブラリを含む) は 1 バイナリなので導入は簡単です。4

sudo curl -L --output /usr/local/bin/grain https://github.com/grain-lang/grain/releases/latest/download/grain-linux-x64
sudo chmod +x /usr/local/bin/grain

grain --help
Usage: grain [options] <file>

Compile and run Grain programs. 🌾

Options:
  -I, --include-dirs <dirs>      add additional dependency include directories (default: [])
  -S, --stdlib <path>            override the standard library with your own (default: "/snapshot/grain/stdlib")
  --initial-memory-pages <size>  initial number of WebAssembly memory pages
  --maximum-memory-pages <size>  maximum number of WebAssembly memory pages
  --import-memory                import the memory from `env.memory`
  --dir <dir...>                 directory to preopen
  --env <env...>                 WASI environment variables
  --elide-type-info              don't include runtime type information used by toString/print
  --release                      compile using the release profile (production mode)
  --no-wasm-tail-call            disables tail-call optimization
  --debug                        compile with debugging information
  --wat                          additionally produce a WebAssembly Text (.wat) file
  --hide-locs                    hide locations from intermediate trees. Only has an effect with `--verbose`
  --no-color                     disable colored output
  --no-gc                        turn off reference counting garbage collection
  --no-bulk-memory               polyfill WebAssembly bulk memory instructions
  --wasi-polyfill <filename>     path to custom WASI implementation
  --no-pervasives                don't automatically import the Grain Pervasives module
  --memory-base <addr>           set the base address for the Grain heap
  --source-map                   generate source maps
  --strict-sequence              enable strict sequencing
  --verbose                      print critical information at various stages of compilation
  -v, --version                  output the current version
  -o <filename>                  output filename
  -h, --help                     display help for command

Commands:
  compile [options] <file>       compile a grain program into wasm
  run [options] <file>           run a wasm file via grain's WASI runner
  lsp [options]                  start the Grain LSP server
  doc [options] <file|dir>       generate documentation for a grain file
  format [options] <file|dir>    format a grain file

適用できるオプションからも WebAssembly ファーストであることがわかりますね。

開発環境としては VSCode 向けの公式拡張機能 vscode-grain が利用可能です。Grain ツールチェイン、vscode-grain、および各種 WASM ランタイムを導入できる DevContainer 定義を作りましたので、ご自由にお使いください:

devcontainer.json
{
    // Docker Image for WasmEdge
    "image": "wasmedge/wasmedge:ubuntu-build-gcc",
    "features": {
        // Install a set of common command line utilities, and set up a non-root user.
        "ghcr.io/devcontainers/features/common-utils:2": {
            "installZsh": "false",
            "username": "vscode",
            "userUid": "1000",
            "userGid": "1000",
            "upgradePackages": "true"
        },
        // Install an up-to-date version of Git, built from source as needed.
        "ghcr.io/devcontainers/features/git:1": {
            "version": "latest",
            "ppa": "false"
        }
    },
    "remoteUser": "vscode",
    "remoteEnv": {
        // Latest release of Grain: https://github.com/grain-lang/grain/releases/latest
        "GRAIN_BIN_URL": "https://github.com/grain-lang/grain/releases/latest/download/grain-linux-x64"
    },
    "onCreateCommand": {
        // Install Grain: https://grain-lang.org/docs/getting_grain
        "grain": "mkdir -p ~/.grain/bin/ && cd ~/.grain/bin/ && curl -sSL ${GRAIN_BIN_URL} -o ./grain  && chmod +x grain && echo 'export PATH=\"$HOME/.grain/bin/:$PATH\"' >> ~/.bashrc",
        // Install latest WasmEdge: https://wasmedge.org/docs/start/install
        "WasmEdge": "curl -sSf https://raw.githubusercontent.com/WasmEdge/WasmEdge/master/utils/install.sh | bash",
        // Install latest Wasmtime: https://docs.wasmtime.dev/cli-install.html
        "wasmtime": "curl https://wasmtime.dev/install.sh -sSf | bash",
        // Install latest Wasmer: https://docs.wasmer.io/install
        "wasmer": "curl https://get.wasmer.io -sSfL | bash",

    },
    "customizations": {
        "vscode": {
            "extensions": [
                // Grain support for Visual Studio Code.
                "grain-lang.vscode-grain"
            ]
        }
    }
}

コンパイルと実行

公式の Hello World を参考にコンパイル&実行してみましょう:

Hello.gr
module Hello

print("Hello, world!")
grain Hello.gr
Hello, world!

無事実行できましたね。この時、Hello.gr.wasm が生成されていることに気が付くことでしょう。生成された WASM は各種 WASM ランタイムで実行可能なはずです。

vscode ➜ /workspaces/grain-fizz-buzz (master) $ wasmtime Hello.gr.wasm 
Hello, world!

vscode ➜ /workspaces/grain-fizz-buzz (master) $ wasmedge Hello.gr.wasm 
[2024-07-09 14:22:15.249] [error] loading failed: illegal opcode, Code: 0x117
[2024-07-09 14:22:15.250] [error]     This instruction or syntax requires enabling Tail Call proposal
[2024-07-09 14:22:15.250] [error]     Bytecode offset: 0x000026b7
[2024-07-09 14:22:15.251] [error]     At AST node: instruction
[2024-07-09 14:22:15.251] [error]     At AST node: expression
[2024-07-09 14:22:15.251] [error]     At AST node: code segment
[2024-07-09 14:22:15.251] [error]     At AST node: code section
[2024-07-09 14:22:15.251] [error]     At AST node: module
[2024-07-09 14:22:15.251] [error]     File name: "/workspaces/grain-fizz-buzz/Hello.gr.wasm"

vscode ➜ /workspaces/grain-fizz-buzz (master) $ wasmer Hello.gr.wasm 
error: Unable to compile "Hello.gr.wasm"
╰─▶ 1: compile error

vscode ➜ /workspaces/grain-fizz-buzz (master) $ wasmer validate Hello.gr.wasm 
error: failed to validate `Hello.gr.wasm`
╰─▶ 1: Validation error: tail calls support is not enabled (at offset 0x26b7)

Wasmtime では実行できましたが、WasmEdgeWasmer では失敗してしまいました。Grain では Tail Call Proposal for WebAssemblyデフォルトで有効としていることが原因です。ランタイム側のサポートを有効にするか、Grain で Tail Call を無効にすると解決します:

# wasmedge は v0.14.0 現在デフォルトで無効
vscode ➜ /workspaces/grain-fizz-buzz (master) $ wasmedge --enable-tail-call Hello.gr.wasm 
Hello, world!

# wasmer は v4.3.4 現在非対応のため、Grain で Tail Call を無効にする
vscode ➜ /workspaces/grain-fizz-buzz (master) $ grain compile --no-wasm-tail-call Hello.gr
vscode ➜ /workspaces/grain-fizz-buzz (master) $ wasmer Hello.gr.wasm 
Hello, world!

Grain で FizzBuzz やってみる

みなさんご存知 FizzBuzz 問題で Grain の文法を概観してみましょう。

なお、これから掲載するコードはこちらのレポジトリにも格納しています。

まずは素直に for ループifで書いてみる

FizzBuzz1.gr
module FizzBuzz1

for (let mut i = 1; i < 101; i += 1) {
  let result = if (i % 3 == 0 && i % 5 == 0) {
    "FizzBuzz"
  } else if (i % 3 == 0) {
    "Fizz"
  } else if (i % 5 == 0) {
    "Buzz"
  } else {
    toString(i)
  }

  print(result)
}

Grain の変数代入(値バインディング)はデフォルトで不変(イミュータブル)です。変更可能な変数を宣言したい場合は mut キーワードを使用します。少し Rust に近いものを感じますが、これは Rust もまた OCaml に影響を受けた言語の 1 つであるためでしょう。

標準ライブラリの List モジュールを使ってみる

FizzBuzz2.gr
module FizzBuzz2

from "list" include List

let list = List.init(100, n => n + 1)
let list = List.map(
  i =>
    if (i % 3 == 0 && i % 5 == 0) {
      "FizzBuzz"
    } else if (i % 3 == 0) {
      "Fizz"
    } else if (i % 5 == 0) {
      "Buzz"
    } else {
      toString(i)
    },
  list
)
List.forEach(n => print(n), list)

メソッドをチェインできない(list.map(fn).forEach(fn) のような書き方はできない)のでこのような形になります。変数 list が複数回宣言されているのはシャドーイングの仕組みによるものです。

カスタム中置演算子でパイプライン演算子を作ってみる

Grain ではカスタム中置演算子(Custom Infix Operators)を定義できます。カスタム中置演算子の作り方は単純で、関数名を () で囲ってあげるだけです:

FizzBuzz3.gr
module FizzBuzz3

from "list" include List

let (|>) = (list, (fn, a)) => fn(a, list)

List.init(100, n => n + 1) |>
  (
    List.map,
    i =>
      if (i % 3 == 0 && i % 5 == 0) {
        "FizzBuzz"
      } else if (i % 3 == 0) {
        "Fizz"
      } else if (i % 5 == 0) {
        "Buzz"
      } else {
        toString(i)
      },
  ) |>
  (List.forEach, n => print(n))

パイプラインっぽくなりました。先ほどのコードより幾分エレガントです。

List.mapList.forEachfn(a, list) の形をとるため、|> 関数で list |> (fn, a) に変形できます。|> 関数は右側にある引数の 1 要素目に関数そのものを取りますが、これは Grain が関数が第一級市民の言語であることを表します。

なおカスタム中置演算子は Haskell や Scala など他の関数型言語にもみられる機能ですが、Python や Kotlin でも使用できるため専売特許というわけではありません。

match 式でもっとエレガントにしてみる

これまでのコードだと、1 イテレーションにつき i % 3 == 0i % 5 == 0 を最悪 2 回ずつ計算することになり美しくありません。せっかくなのでもっとエレガントにしてあげましょう:

FizzBuzz4.gr
module FizzBuzz4

from "list" include List

let (|>) = (list, (fn, a)) => fn(a, list)

List.init(100, n => n + 1) |>
  (List.map, i => match ((i % 3, i % 5)) {
    (0, 0) => "FizzBuzz",
    (0, _) => "Fizz",
    (_, 0) => "Buzz",
    _ => toString(i),
  }) |>
  (List.forEach, n => print(n))

Grain では(先行する言語たちと同様に)match 式がパターンマッチングと網羅性検査の仕組みを提供します。以下は公式拡張機能を導入した VSCode でパターンを網羅できていないコードを表示したところです:

grain_matching.png

ちゃんとコンパイラが怒ってくれています。網羅できていないケースの例示など、エラーメッセージがわかりやすい点に好感が持てますね。

また、| で複数パターンにマッチさせることもできます:

FizzBuzz5.gr
module FizzBuzz5

from "list" include List

let (|>) = (list, (fn, a)) => fn(a, list)

List.init(100, n => n + 1) |>
  (List.map, i => match (i % 15) {
    3 | 6 | 9 | 12 => "Fizz",
    5 | 10 => "Buzz",
    0 => "FizzBuzz",
    _ => toString(i),
  }) |>
  (List.forEach, n => print(n))

別解として Match Guards も使えますが、今回のケースではかえって文字数が多くなってしまいました:

FizzBuzz6.gr
module FizzBuzz6

from "list" include List

let (|>) = (list, (fn, a)) => fn(a, list)

List.init(100, n => n + 1) |>
  (List.map, i => match (i % 15) {
    r when [3, 6, 9, 12] |> (List.contains, r) => "Fizz",
    r when [5, 10] |> (List.contains, r) => "Buzz",
    0 => "FizzBuzz",
    _ => toString(i),
  }) |>
  (List.forEach, n => print(n))

なお、個人的には FizzBuzz4.gr のタプルを使った実装が好みです。

まとめと感想

というわけで Grain でいくつかの FizzBuzz をやってみましたが、なかなか書き味の良い言語でしたし、触っていて楽しかったです。カスタム中置演算子は制約の多い言語だからこそわかりやすく輝きを見せており、今回得られた経験は他の言語でプログラミングを行う際にも活かせそうです。「違うものにふれると、より『そのもの』のことがわかる」と言いますし、「プログラミング言語は単なる技術ではなく、プログラマーがそれを道具として思考するもの」とも言います。普段は他の言語を愛好する皆様も新しい言語とふれあって見聞を広めてみてはいかがでしょうか。

参考リンク

  1. GitHub と Stack Overflow での人気度から算出するプログラミング言語の人気度ランキング。開発者に焦点を当てたアナリストファームの RedMonk が 2012 年から公開しています。

  2. Stack Overflow Developer Survey 2023 には登場しませんし、TIOBE Index では集計対象外です。

  3. Melange で JavaScript にコンパイルできるほか、OCaml コンパイラでネイティブにもコンパイル可能です。

  4. 公式ガイドと異なり、バイナリ URL として最新リリース資産のダウンロードリンクを指定しています。

6
2
0

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
6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?