その他の便利な機能

パイプ演算子

関数は通常、引数の前に書かれます。これはプログラムを左から右に読む際に、関数の 出力 が最重要だという見方を助長します。つまり、関数は達成すべきゴール(つまり計算すべき値)を持っており、そのプロセスをサポートするために引数を受け取るという視点です。しかし、プログラムの中には出力の生成のために入力が逐次的に改良されていく、という観点のほうが理解しやすいものも存在します。このような状況に対して、LeanはF#にあるものと似た パイプライン 演算子を提供しています。パイプライン演算子はClojureのスレッディングマクロと同じ状況化で有用です。

パイプライン E1 |> E2E2 E1 の省略形です。例えば、以下を評価すると:

#eval some 5 |> toString

以下の結果になります:

"(some 5)"

このように強調箇所を変えることでより読みやすくなるプログラムもありますが、多くのコンポーネントを含む場合にパイプラインはその本領を発揮します。

以下の定義に対して:

def times3 (n : Nat) : Nat := n * 3

以下のパイプラインは:

#eval 5 |> times3 |> toString |> ("It is " ++ ·)

以下を出力します:

"It is 15"

より一般的には、パイプラインの列 E1 |> E2 |> E3 |> E4 は関数適用のネスト E4 (E3 (E2 E1)) の省略形です。

パイプラインは反対向きに書くこともできます。この場合、変換する対象のデータを先に持ってきません;しかし、入れ子になった括弧が多くて読者が困るような場合にはパイプラインによって適用のステップを明確にすることができます。先ほどの例は以下の記述と等価です:

#eval ("It is " ++ ·) <| toString <| times3 <| 5

これは以下の短縮形です:

#eval ("It is " ++ ·) (toString (times3 5))

Leanのメソッドのドット記法は、ドットの前の型名を使ってドットの後ろの演算子の名前空間を解決するもので、パイプラインと似た目的を提供しています。仮にパイプライン演算子が無くとも、[1, 2, 3].reverse の代わりに List.reverse [1, 2, 3] と書くことは可能です。しかし、パイプライン演算子はドットを付けた関数をたくさん使う場合でも便利です。([1, 2, 3].reverse.drop 1).reverse[1, 2, 3] |> List.reverse |> List.drop 1 |> List.reverse と書くこともできます。この書き方では、ただ引数を受け取るという理由だけで式を括弧で囲む必要がなく、KotlinやC#のような言語での便利なメソッドチェーンを再現しています。ただし、この場合は名前空間を手動で指定する必要があります。そこで究極的な便利機能として、Leanは「パイプラインドット」演算子を提供しています。これは関数をパイプラインのようにグループ化しますが、名前空間を解決するために型名を使用します。「パイプラインドット」を使用すると、上記の例は [1, 2, 3] |>.reverse |>.drop 1 |>.reverse と書き換えることができます。

無限ループ

do ブロックの中で、repeat キーワードを使うと無限ループを作れます。例えば、"Spam!" という文字列を連投するプログラムで以下のように使われます:

def spam : IO Unit := do
  repeat IO.println "Spam!"

repeat ループは for ループと同じように breakcontinue をサポートしています。

feline の実装 における dump 関数は再帰関数を使って永遠に実行していました:

partial def dump (stream : IO.FS.Stream) : IO Unit := do
  let buf ← stream.read bufsize
  if buf.isEmpty then
    pure ()
  else
    let stdout ← IO.getStdout
    stdout.write buf
    dump stream

この関数は repeat を使うことで劇的に短縮できます:

def dump (stream : IO.FS.Stream) : IO Unit := do
  let stdout ← IO.getStdout
  repeat do
    let buf ← stream.read bufsize
    if buf.isEmpty then break
    stdout.write buf

spamdump も、それ自体が無限再帰ではないため partial として宣言する必要はありません。その代わりに、repeatForM インスタンスが partial である型を使用します。関数の部分性は、関数の呼び出し時に「感染」することはありません。

whileループ

局所的な可変性を使ってプログラムを書く場合、while ループは ifbreak をガードするような repeat よりも便利です:

def dump (stream : IO.FS.Stream) : IO Unit := do
  let stdout ← IO.getStdout
  let mut buf ← stream.read bufsize
  while not buf.isEmpty do
    stdout.write buf
    buf ← stream.read bufsize

裏では、whilerepeat をよりシンプルにした表記にすぎません。