for/while/if/return を使わずにプログラムを書く
- #program
今日は普段の開発でよく使う for / while / if / return を一切使わずにプログラムを書きたいと思います。
不正の無いように、これらを使えない言語で書きます。
Gleam という関数型プログラミング言語があります。
Gleam は関数型言語版の Go と言われており、言語機能が非常にシンプルです。
この言語にはforもwhileもifもreturnもありません。 1
また、以下のような特徴があります。
- 例外(
Exception)が無い - 0除は0 (!?) 参考
- ヌルポインタが無い
- trait や型クラスが無い
- 全ての変数がイミュータブル
- パイプライン演算子(
|>)が使える - Erlang または JavaScript のソースコードにコンパイルできる
そんな言語でどうやってプログラムを書いていくのかを見て行きましょう。
ここには様々な関数型プログラミングのエッセンス・技術が含まれていると思います。
簡単な例(階乗計算)
まずは簡単な例として階乗計算を見ていきます。
階乗計算には繰り返しや分岐が必要ですが、繰り返しは再帰呼び出しで実現可能です。
Gleam では条件分岐は全てcaseによるパターンマッチで行います。
また、関数型言語では一般的ですが、最後の式が戻り値になります。
以下は10の階乗を求めるプログラムです。
import gleam/int
import gleam/io
// main がエントリーポイント
// Nil は返す値が無い場合の型(ユニット型)です。
// Gleam や一般的な関数型言語では何らかの値を必ず返す必要があります。(voidみたいなのは無い)
pub fn main() -> Nil {
10
|> factorial()
|> int.to_string()
|> io.println()
}
pub fn factorial(n: Int) -> Int {
factorial_loop(n, 1)
}
fn factorial_loop(n: Int, acc: Int) -> Int {
case n {
0 -> acc
_ -> factorial_loop(n - 1, acc * n)
}
}
実行結果
❯ gleam run
Compiled in 0.01s
Running factorial.main
3628800
上記のコードは少し冗長なので、実際には畳み込みを使うことの方が多い気がします。
畳み込みを使うと、次のように書けます。
import gleam/int
import gleam/io
import gleam/list
pub fn main() -> Nil {
10
|> factorial2()
|> int.to_string()
|> io.println()
}
pub fn factorial2(n: Int) -> Int {
list.range(1, n)
|> list.fold(1, fn(x, acc) { acc * x })
}
早期リターン
関数型言語ではあまり使わない印象がありますが、早期リターン(early return)をしたい場合はどうすれば良いでしょうか?
Gleam には高階関数をフラットに書けるuseという構文があります。
それを使えば、早期リターンが実現できそうなことがわかりますね。
実際はboolパッケージにあるguardを使って下記のように書けます。
因みに、guardの実装はとても単純です。
import gleam/bool
import gleam/int
import gleam/io
pub fn main() -> Nil {
say_number(9)
say_number(10)
say_number(11)
say_number(12)
}
pub fn say_number(number: Int) -> Nil {
// number が10を超える場合は早期リターン
use <- bool.guard(when: 10 < number, return: Nil)
// <> は文字列連結
io.println("The number is " <> number |> int.to_string())
}
実行結果
❯ gleam run
Compiled in 0.03s
Running guard.main
The number is 9
The number is 10
複数の型で共通処理
trait や型クラスが無ければ、どのようにして複数の型に共通した処理を書くのでしょうか?
特別な言語機能は必要ありません。値と関数があれば事足ります。
まず、インターフェースとなる型と、それを使用する関数を定義します。
pub type Drawable {
Drawable(draw: fn() -> String)
}
pub fn render(d: Drawable) -> String {
d.draw()
}
次に、この特性を持たせたい型とそれをインターフェース型に変換する関数を定義します。
pub type Circle {
Circle(radius: Int)
}
pub fn circle_to_drawable(self: Circle) -> Drawable {
Drawable(draw: fn() {
"Drawing a circle of radius " <> int.to_string(self.radius)
})
}
pub type Rectangle {
Rectangle(width: Int, height: Int)
}
pub fn rectangle_to_drawable(self: Rectangle) -> Drawable {
Drawable(draw: fn() {
"Drawing a rectangle "
<> int.to_string(self.width)
<> "x"
<> int.to_string(self.height)
})
}
使用方法はこんな感じになります。
pub fn main() -> Nil {
let d = circle_to_drawable(Circle(10))
io.println(render(d))
// パイプライン演算子を使うとこのように書ける
Rectangle(20, 30)
|> rectangle_to_drawable()
|> render()
|> io.println()
}
実行結果
❯ gleam run
Compiling trait
Compiled in 0.39s
Running trait.main
Drawing a circle of radius 10
Drawing a rectangle 20x30
コラッツ問題
最後に Exercism という無料で使えるプログラミング学習サイトを使って、簡単な問題を解いてみましょう。
今回はコラッツ問題を解いてみます。
コラッツ問題は 任意の正の整数 n に対して、以下の操作を値が1になるまで繰り返し行い、掛かった操作数を求める問題です。
- n が偶数の場合、n を 2 で割る
- n が奇数の場合、n に 3 を掛けて1 を足す
問題をダウンロードすると以下のテンプレートが与えられます。
あらかじめエラーの型を用意してくれており、また、関数の型も決まっている為、todo を埋めるだけです。
因みに Result は関数型言語や Rust 等では一般的に利用されている、成功または失敗のいずれかの値を持つ型です。
括弧内の左に成功時の型、右に失敗時の型を指定し、成功時はOk(value)、失敗時はError(value)といった形で値を生成します。
pub type Error {
NonPositiveNumber
}
pub fn steps(number: Int) -> Result(Int, Error) {
todo
}
number が 0 以下の場合は NonPositiveNumber エラーとし、それ以外の場合は再帰で求められそうですね。
Gleam では1つのcase式で複数の値を同時にパターンマッチすることが可能です。
それを利用すると以下のように解けます。
pub type Error {
NonPositiveNumber
}
pub fn steps(number: Int) -> Result(Int, Error) {
case number <= 0 {
True -> Error(NonPositiveNumber)
False -> Ok(loop(0, number))
}
}
fn loop(acc: Int, n: Int) -> Int {
case n, n % 2 == 0 {
1, _ -> acc
n, True -> loop(acc + 1, n / 2)
n, False -> loop(acc + 1, n * 3 + 1)
}
}
テスト結果 (0 failed なので正解)
❯ gleam test
Compiled in 0.29s
Running collatz_conjecture_test.main
......
Ran 6 tests, 0 failed
おわりに
今回は「for / while / if / return を一切使わずにプログラムを書く」というテーマで Gleam という関数型言語をご紹介しました。
Gleam の言語機能 は半日程度で理解できるぐらいシンプルですが、シンプルな言語機能だけでプログラムを書くには様々な工夫が必要ということがわかりました。
Gleam は2024年3月に Version 1.0 ができたばかりのまだまだ新しい言語ですが、GitHubのスターは2026年1月時点で21kを超えており、そこそこ受け入れられているのではないかと思います。
私の普段の仕事で Gleam のような言語を使う日は恐らく来ないと思いますが、こういった関数型言語のエッセンス・技術は我々が普段使いしている言語にも徐々に取り入れられているように思いますので、今から学んでおくのも良いのではないでしょうか。
興味がある方は Exercism に100以上の練習問題があり、全て無料で解けるので試してみてはいかがでしょうか。
以上です。
Footnotes
-
厳密には case の条件(guard)としての if はあります。 ↩