💍

Onyx の関数型な機能 - Pipe oprator (パイプ演算子) |>

2023/12/28に公開

Onyx はデータと手続きを組み合わせることやその挙動に多態性を作るようなオブジェクト指向っぽいことも冗長ながら一応できます。しかしオブジェクト指向の言語ではありません。他方で関数型的な要素も少ないです。作者が関数型的な機能の例として上げているものが2つあります。

  1. Pipe oprator (パイプ演算子)
  2. Quik procedures

ここでは Pipe oprator について扱います。

Pipe oprator (パイプ演算子) |>

値や式の評価結果を次の呼び出しの第一引数として挿入します。例えば、

x |> f(y)f(x, y)

という感じに変換されます。これはチェーンすることができるため、深くネストされた関数呼び出しをフラットに見やすくすることが可能です。

use core {*}

main :: () {
    0 .. 1000
    |> iter.as_iter()
    |> iter.skip_while(x => x < 100)
    |> iter.filter(x => x % 2 == 0)
    |> iter.take(10)
    |> iter.collect()
    |> println();
}
$ onyx run main.onyx     
[ 100, 102, 104, 106, 108, 110, 112, 114, 116, 118 ]

レンジオブジェクトをイテレータ化して、100未満を捨てて、偶数だけにフィルタして、最初の10要素だけ残して、配列に変換して、プリントしてます。

上記のコードは以下と等価です。

use core {*}

main :: () {
    println(iter.collect(iter.take(iter.filter(iter.skip_while(iter.as_iter(0 .. 1000), x => x < 100) , x => x % 2 == 0), 10)));
}

改行が入っていないというのはありますが、ネストしているよりはるかに読みやすいですね。 Pipe oprator は関数呼び出しのネストをフラットにする構文糖衣(Syntax Sugar)だともいえます。

感想

機能としては普通に便利だと思います。ただ、 Pipe operator を指して「関数型の機能」と呼ばれるとそうのか?という感じがします。自前で関数を書くときの引数設計に影響するので、 Pipe operator が第一引数に値を展開する、ルールは覚えておくべきでしょう。

参考ドキュメント

余談 - Clojure のスレッディングマクロ

上記を Clojure のスレッディングマクロで書くとこうなります。

(->> (range 0 1000)
     (drop-while #(< % 100))
     (filter #(= 0 (mod % 2)))
     (take 10)
     vec
     println)

Onyx の |> が第一引数に値を展開するという意味ではスレッドファースト(->)が同じですが、ここではスレッドラスト ->> を使っています[1]。書き味や見た目は一緒ですね。機能性以外のところでいうと、スレッディングマクロの場合は単なる1マクロとして実装されていますが、 Pipe Operator の場合は言語機能かつ演算子として実装されているというのも違いです。

参考: Clojure - Threading Macros Guide

脚注
  1. Clojure のスレッディングマクロには、前の評価値を次の関数適用の第一引数に展開する -> と末尾の引数に展開する ->> があります。Clojureでは通常シーケンスを処理する関数は末尾の引数でそのシーケンスを受け取る設計のため、 ->> を使うことになります。 ↩︎

Discussion