Juliaは、JITコンパイルできる関数の中に、できるだけ多くのコードを書くことを推奨しています。そして、小さな関数をたくさん作ることは、設計上、性能が良くなるようにできています。
そうすると、小さくて単純な関数がたくさん残ることになり、それらを組み合わせて、簡単ではない処理をこなす必要が出てきます。
わかりやすい方法の1つは、関数呼び出しを入れ子にすることです。次の例はかなり無理やりですが、要点は伝わるでしょう。
julia> first.(titlecase.(reverse.(["my", "test", "strings"])))
3-element Vector{Char}:
'Y': ASCII/Unicode U+0059 (category Lu: Letter, uppercase)
'T': ASCII/Unicode U+0054 (category Lu: Letter, uppercase)
'S': ASCII/Unicode U+0053 (category Lu: Letter, uppercase)
この方法の欠点は、入れ子が深くなるほど読みやすさが急速に落ちることです。
もっと単純で、もっと柔軟な方法が必要です。
これは数学者に愛用されている手法で、Juliaはその数学的な構文を取り入れています。
任意の数の関数を、∘演算子(\circと入力してからタブ)でcomposedできます。その結果は、1つの関数として使えます。
julia> compfunc = first ∘ titlecase ∘ reverse
first ∘ titlecase ∘ reverse
julia> compfunc.(["my", "test", "strings"])
3-element Vector{Char}:
'Y': ASCII/Unicode U+0059 (category Lu: Letter, uppercase)
'T': ASCII/Unicode U+0054 (category Lu: Letter, uppercase)
'S': ASCII/Unicode U+0053 (category Lu: Letter, uppercase)
# alternative syntax, giving the same result
julia> (first ∘ titlecase ∘ reverse).(["my", "test", "strings"])
注意すべき点がいくつかあります。
もう1つの方法は、_数学者_のアプローチというより、_プログラマー_のアプローチと言えるかもしれません。
Pipelinesは、Unixのシェルスクリプトで昔から使われており、最近では主要なプログラミング言語でも一般的になりました(その採用を先駆けたのはF#だとよく言われます)。
基本的な考え方は、何らかのデータから始めて、それを一連の関数に通して結果を得るというものです。
パイプ演算子は|>です(F#や最近のバージョンのRと同じです)。ただしJuliaには、ブロードキャスト版の.|>もあります。
julia> ["my", "test", "strings"] .|> reverse .|> titlecase .|> first
3-element Vector{Char}:
'Y': ASCII/Unicode U+0059 (category Lu: Letter, uppercase)
'T': ASCII/Unicode U+0054 (category Lu: Letter, uppercase)
'S': ASCII/Unicode U+0053 (category Lu: Letter, uppercase)
実行は厳密に左から右になり、各関数の出力が矢印の方向に流れて、次の関数の入力になります。
合成とパイプラインの説明に使った関数が、どれも_1つ_の引数しか取らないのは、偶然ではありません。
一部の純粋関数型言語では、_最初の_引数を関数にパイプしつつ、他の引数も含めることができます。
対照的に、Juliaはパイプラインの中で関数の_名前_(またはそれに相当するもの)だけを想定していて、追加の引数は取りません。
これには重要な技術的な理由があります(curryingが言語設計の標準的な一部ではないことに関係しています)。curryingを理解していない_多く_の人たちは、この制約が不注意な見落としではなく、今後のJuliaのバージョンでも変わる見込みはない、と受け入れるだけでよいでしょう。
必要なことを何でもしてくれる単一引数の関数が必要です。幸いなことに、Juliaで新しい関数を定義するのは簡単です。
最も簡単には、anonymous functionを使う方法があります。たとえば、入力が文字列1つで、それをアンダースコアで分割したいとします。
julia> "my_test_strings" |> (s -> split(s, '_'))
3-element Vector{SubString{String}}:
"my"
"test"
"strings"
そのベクトルは、前と同じように、他の関数にパイプできます。
この場合、無名関数を括弧で囲むのは任意ですが、一般には曖昧さを減らすのに役立つ方法です。
同様に、プログラムのもっと前のほうで名前付きの関数を作り、必要に応じて再利用することもできます。
この演習では、高校時代の恋人同士がSNSで愛を告白できるように、2人のイニシャルを使ったUnicodeのハートを作ります。
❤ J. + M. ❤
cleanupnameメソッドを実装します。
名前を受け取り、その中の-をすべて取り除いて、スペースに置き換えます。
また、名前の先頭と末尾にある空白も取り除きます。
julia> cleanupname("Jane-Ann")
"Jane Ann"
firstletterメソッドを実装します。
名前を受け取り、その最初の文字を文字列として返します。
前の手順で定義したcleanupnameを再利用し、他の関数と組み合わせてみましょう。
julia> firstletter("Jane")
"J"
initialメソッドを実装します。
名前を受け取り、その最初の文字を大文字にし、そのあとにピリオドを付けて返します。
前の手順で定義したfirstletterを再利用しましょう。
initial("Robert")
"R."
coupleメソッドを実装します。
2つの名前を受け取り、それぞれのイニシャルを絵文字のハートで囲んで返します。
前の手順で定義したinitialを再利用しましょう。
couple("Blake Miller", "Riley Lewis")
"❤ B. + R. ❤"