1274 文字
6 分
GleamでEarly-returnしたい
2024-09-30

Gleamはif/else文・式やreturnと​いった​構文を​持たないので,​caseと​パターンマッチングのみで​条件分岐を​表現し,​関数の​最後の​式で​評価された​値を​返り値と​する.なかなかに​思想が​強い.

例と​して,​何かしらの​IDを​バリデーションしたい​場面が​あったとする.

IDの​形式はID-XXXX(Xは​数字)とし,​成功したら​IDの​数字部分を​パースした​ものを,​失敗したら​フォーマットが​不正と​いう​エラーを​返すような​形に​する.

仮に​caseだけを​使って​書くとしたら,​このような​見た​目になる.

const id_prefix = "ID-"
fn validate_id(str: String) -> Result(Int, String) {
	case str |> string.startswith(id_prefix) {
        True -> {
			case str |> string.replace(id_prefix, "") |> int.parse {
				Ok(value) -> Ok(value)
				Error(_) -> Error("IDの数字部をパース出来ませんでした")
			}
		}
		False -> Error("IDの形式が不正です")
	}
}
pub fn main() {
	validate_id("ID-1234")
	|> io.debug
	
	validate_id("ID-ABCD")
	|> io.debug
	
	validate_id("1234")
	|> io.debug
}

case-only

これでも​動く​ことには​動くが,​今後​IDに​桁数の​制約や​その​他の​条件が​増えたりした​場合,caseが​どんどんネストしていき,​コードが​絶望的に​見にくくなるだろう.

hadoken

それと,​6行目のOk(value) -> Ok(value)も​なんだか​冗長な​気が​する.と​いうか正しい​値を​返す文は​分岐の​真ん中ではなく,​一番​最後に​置きたい​(これは​思想の​問題かもしれないが​).

こうなると​Early-returnしたくなる.が,​Gleamにはifelsereturnも​ないのである!

どう​した​ものか…と​悩んでいた​ところ,bool.guardと​いう​関数が​ある​ことを​知った.

Gleamの​Tips#66685d444293a40000d73ba3

gleam/bool · gleam_stdlib · v0.40.0

Run a callback function if the given bool is False, otherwise return a default value.

With a use expression this function can simulate the early-return pattern found in some other programming languages.

関数の​説明にもearly-returnと​記述が​ある.​読み進めると

In a procedural language:

if (predicate) return value;
// ...

In Gleam with a use expression:

use <- guard(when: predicate, return: value)
// ...

と​あり,​一般的な​手続き型言語に​おける​Early-returnのように​使えるようだ.

早速これで​最初の​例を​書き換えてみる.

fn validate_id(str: String) -> Result(Int, String) {
	use <- bool.guard(
		str |> string.starts_with(id_prefix) |> bool.negate,
		Error("IDの形式が不正です")
	)
	
	use id_num <- result.try(
		str
		|> string.replace(id_prefix, "")
		|> int.parse
		|> result.replace_error(
			"IDの数字部をパース出来ませんでした"
		),
	)
	
	Ok(id_num)
}

若干の​書き換えは​あった​ものの,​最初の​例と​比べて,

  • インデントが​浅くなった
  • エラーを​最初に​蹴る​ことで​関数の​一番​最後に​正しい​値を​持って​これた

ことに​より​コードの​見通しが​良くなった.

めで​たしめでたし.


…な​ぜ​こんな​ことが​出来たのか?​use周りの​理解が​浅かったので​いろいろと​調査した.

use式より​後ろに​書かれた​コードは​useで​展開した​関数(ここではgleeting())の​コールバック関数(name: fn() → String)の​中に​渡される.

fn gleeting(name: fn() -> String) -> Nil {
	let message = "Hello " <> name() <> "!"
	io.println(message)
}

pub fn main() {
	use <- gleeting()
	"Gleam"
}

この​とき,use <- gleeting()より​後ろに​書かれた​”Gleam”と​いう​文字列は​単なる​文字列と​してではなく,fn () { “Gleam” }と​いう​無名関数に​なり,​gleeting関数の​name引数に​コールバック関数と​して​渡される.

また,​useは​式なので,​useで​展開した​関数の​コールバックは​ブロックに​囲むことで​変数に​束縛したり,​囲まずに​そのままに​して​関数の​返り値と​する​ことも​できる.

fn gleeting(name: fn() -> String) -> String {
	"Hello " <> name() <> "!"
}

pub fn main() {
	let message = {
		use <- gleeting()
		"Gleam"
	}
	io.println(message)
}

ここでは,​上の​例の​gleeting関数の​返り値をNilからStringに​し,​その​コールバックを​変数に​束縛している.


さて,bool.guardに​話を​戻す.bool.guardは​このように​定義されている.

pub fn guard(
	when requirement: Bool,
	return consequence: t,
	otherwise alternative: fn() -> t,
) -> t {
	case requirement {
		True -> consequence
		False -> alternative()
	}
}

when(requirement)が

  • Trueの​時には​コールバックは​実行されずreturn(consequence)引数に​渡された​値を
  • Falseの​時には​コールバック(otherwise())を​実行し​その​返り値を

それぞれ返す.

Trueの​時に​コールバックを​実行しないで​値を​返す​ことで,​Early-returnを​実現していると​いう​ことだった.

敢えて​useを​使わずに​同じ​動作を​する​コードを​書いた​場合,​このような​見た​目になる.

bool.guard(
	when: str |> string.starts_with(id_prefix) |> bool.negate,
	return: Error("IDの形式が不正です"),
	otherwise: fn() {
		result.try(
			str
				|> string.replace(id_prefix, "")
				|> int.parse
				|> result.replace_error(
					"IDの数字部をパース出来ませんでした"
				),
			apply: fn(id_num) { Ok(id_num) },
		)
	}
)

useを​使う​ことで,​コールバックに​渡している​処理を​ネストする​ことなく​書ける,と​いう​カラクリだった.

だいぶ寄り道した感が​あったが,​Gleamへの​理解が​より​深まった.

参考

Gleamの​useに​ついて Gleamの​Tips#66685d444293a40000d73ba3

GitHubで編集を提案