Edited at

Elmなど関数型言語における引数定義の順序についてのガイドライン

関数定義の際に引数の順序に迷うことがあった。関数型プログラミングにはうん十年という長い歴史があるのでどういう風に引数を配置すればより汎用性の高い関数が定義できるのか議論があるはずだと思い調べたところ、ガイドラインなるものを見つけたため紹介したい


引数定義の順序についてのガイドライン


For languages that support currying and partial-application easily, there is one compelling series of arguments, originally from Chris Okasaki:


カリー化と部分適用を簡単にサポートする言語については、元々Chris Okasakiからの説得力のある一連の議論があります。

Ordering of parameters to make use of currying | stackoverflow

とまあお堅いものがあったのだけれど、F#の記事の中での語り口の方がわかりやすかったためそちらを取り上げてみる



  1. Put earlier: parameters more likely to be static

  2. Put last: the data structure or collection (or most varying argument)

  3. For well-known operations such as “subtract”, put in the expected order



  1. 静的である可能性の高い引数ほど前に

  2. データ構造やコレクション、または変わりやすい引数は最後に

  3. 引き算のような広く知られた操作は想定される順番に

Designing functions for partial application | fsharpforfunandprofit.com

以下に解説を試みる


1. 静的である可能性の高いパラメータほど前に

部分適用をより容易にするため。具体的にはデフォルト値が簡単に設定できるようになる、以下F#の記事をelmに書き直したもの


Logger.elm

adderWithPluggableLogger : (String -> number -> number) -> number -> number -> number

adderWithPluggableLogger logger x y =
let
_ =
logger "x" x

_ =
logger "y" y

_ =
logger "x+y" x + y
in
x + y

consoleLogger : String -> a -> a
consoleLogger =
Debug.log

watchLogger : String -> a -> a
watchLogger =
Debug.watch

addWithConsoleLogger : number -> number -> number
addWithConsoleLogger =
adderWithPluggableLogger consoleLogger

-- addWithConsoleLogger 1 2
-- addWithConsoleLogger 42 99

addWithWatchLogger : number -> number -> number
addWithWatchLogger =
adderWithPluggableLogger watchLogger

-- addWithWatchLogger 1 2
-- addWithWatchLogger 42 99


この場合のデフォルト値はロガーの種類。

Debug.watchは現在のElmのバージョンではなくなっているけれど、よりF#記事の原文に近いニュアンスを出すために採用した

ちなみに、numbercomparableappendableはElmにおいてただの型変数ではなく特殊な型変数として扱われる、以下詳細

コラム:Haskellから見たElm #余談:elmの特別な型 | プログラミング言語Elm本

さらに

add42WithConsoleLogger : number -> number

add42WithConsoleLogger =
addWithConsoleLogger 42

-- [1,2,3] |> List.map add42WithConsoleLogger
-- [1,2,3] |> List.map add42 //compare without logger

この場合はconsoleLoggerと共に足し算の左辺に42をデフォルト値として設定している

すなわち、「静的である可能性の高い引数ほど前に」->「デフォルト値を設定してその関数を使い回したい引数ほど前に」と読み替えられるんじゃないだろうか。

あるいは、「静的である可能性が高い」->「固定されることが多い」だと読み替えてもわかりやすいかもしれない

P.S.

「適用する値が決まるのが早いパラメータほど前に」の方がしっくりくるというお声をいただいた。部分適用って先に引数の一部を埋めてしまうことであるので、先に値が決まっている引数を適用するのが道理で、かつ静的なパラメータはコンパイル時に値が決まっているので前にきている、というのも納得だった


2. データ構造やコレクション、または変わりやすい引数は最後に

これはパイプで繋いだり関数合成したりをしやすくするため。Listモジュールの関数がわかりやすい

result =

List.range 1 10 -- -> [1..10]
|> List.map (\i -> i + 1)
|> List.filter (\i -> i > 5)

また、Elmでも他の関数型言語にあるような引数省略の記法を使うことができる

compositeOp =

List.map (\i -> i + 1) >> List.filter (\i -> i > 5)

result =
compositeOp (List.range 1 10)


3. 引き算のような広く知られた操作は想定される順番に

引き算が右辺 - 左辺みたいになっちゃダメよという話

-- Subtract numbers like `4 - 3 == 1`.

sub : number -> number -> number
sub =
Elm.Kernel.Basics.sub

-- sub 4 3 -> 1


「嗚呼(ああ)、引数の順番が変えたい」と思ったときはどうすればいいのか

方法は2つ


  1. ラップする関数を作る

  2. 無名(匿名)関数、特にElmではラムダ式を用いる。

以下、stackoverflowの事例をもじったもので解説する

import Color exposing (red, blue, yellow)

import Graphics.Element exposing (Element, color, spacer, flow, right)

colors = [ yellow, red, blue ]

presentColors : List Element
presentColors = List.map (color someColor (spacer 30 30)) colors

main : Element
main =
flow right presentColors

補足

spacer : Int -> Int -> Element

-- Create an empty box. This is useful for
-- getting your spacing right and for making borders.
-- 空のボックスを作る。スペースを正しくすること、境界線を作ることを助けてくれるよ

color : Color -> Element -> Element
-- Create an Element with a given background color.
-- 与えられた背景色の要素を作ってくれるよ


However as you can see the function color takes the color argument first and so I cannot create a partially applied version of it for List.map to use.

So how can I flip the arguments to color so that it can be partially applied?


ただし、ご覧のとおり、color関数は最初に色の引数を取るため、List.mapが扱えるように部分的に適用したバージョンを作成して使用することはできません

では、引数をcolor関数に部分適用できるようにするにはどうすればよいですか?

flip arguments to Elm function call | stackoverflow


1. ラップする関数を作る

方法の1つ目は

flipedColor : Element -> Color -> Element

flipedColor element someColor =
color someColor element

みたいなflipedな(ひっくり返した)ラップ関数を定義してあげること。


2. 無名(匿名)関数、特にElmではラムダ式を用いる。

2つ目はstackoverflowでも触れられている通り

List.map (\c -> color c (spacer 30 30)) colors

みたいに匿名関数をラムダ式を用いて定義し、List.mapの第一引数に渡してあげること。

ちなみに、stackoverflowの回答にflip関数を使うというものがあるけれど、flip関数はcurry関数やuncurry関数と共にElm0.19から廃止されている


人類には早かった。


Elm 0.19 の主な変更点 | ジンジャー研究室


Appendix: flip関数

まあ、なければ自分で作ればいいのだけれど

flip : (a -> b -> c) -> b -> a -> c

flip function argB argA =
function argA argB

ちなみに、jsでもflip関数は簡単に作ることができる

const flip = fn => a => b => fn(b)(a);

ここまで、関数型プログラミングにおける引数定義の順番のガイドラインについて紹介させていただいた、この分野はつよつよエンジニアの方々が蠢き犇く分野だと思うので、間違っているところ、あるいはもっとよくできるところがあればどんどんマサカリを投げていただきたい所存である(

以上。