読者です 読者をやめる 読者になる 読者になる

const をめぐるあれこれ

Haskell

Prelude には,const :: a -> b -> a という関数が定義されています.a -> b -> a という型は a -> (b -> a) のことなので,この関数を引数に適用すると,b -> a という型の関数になります.できあがった関数はどのような値 v :: b に適用しても,この関数を作るときに適用した引数を返します.

ghci> :type const
const :: a -> b -> a
ghci> let k1 = const 1
ghci> k1 "Hoge"
1
ghci> k1 False
1
ghci> k1 undefined
1
ghci> let kHuga = const "Huga"
ghci> kHuga "Hoge"
"Huga"
ghci> kHuga 2
"Huga"
ghci> kHuga undefined
"Huga"

さて「関数の意味は解ったけれど,こんな関数使い道あるのか」と思いませんでしたか.実はすごく基本的なところで良く使うんですよ.だから Prelude にはいっているわけです.const は 2つの選択肢から最初のものを選択するものです.const の「ミソ」は,たとえ選択するのは常に1つめであっても,実際に選択するためには2つめの選択肢が必要であるということです.

こんな関数を調べてみましょう.

  1. takeWith : 1つめの引数のリストを2つめの引数のリストの長さ分だけ取り出したもの
  2. dropWith : 1つめの引数のリストを2つめの引数のリストの長さ分だけ切り落したもの
takeWith :: [a] -> [b] -> [a]
takeWith = zipWith const
┏┳┳┳┳┳┳┓
┗┻┻┻┻┻┻┛
├──┤
┌┬┬┐
└┴┴┘
   ↓
┏┳┳┓
┗┻┻┛

2つめの引数のリストを定規のように使っているのがわかりますね.では dropWithの方はどうでしょう.

dropWith :: [a] -> [b] -> [a]
dropWith = foldl' (const . tail) 

これは2つめの引数リストの長さと同じ回数だけ tail を適用しています.このようにリストをカウンタ代わりに使えるのには理由があって,リストは内容つまり要素を無視すると,自然数と同じ構造になります.自然数の役割はまさにカウンタであり,同じ構造をもつリストがその代替として使えるというわけです.const は 2つめの引数の存在は見るけれども,内容は見ないので,まさにこのような用途に使えます.

また,よく知られている length は以下のようになります.

length :: [a] -> Int
length = foldl' (const . succ) 0

const の型 a -> b -> a をよく観察してみましょう.1つめの a と2つめの a は同じ型ですが,同じ値とは限らないではないですか?
a や b は型変数です.ということは const は引数の値の構成について何の情報も利用できないということです.つまり,a -> b -> a という型の関数は,最初の a 型の引数の値と b 型の引数の値とから,(undefined を返すような病的なものでないかぎり)別の a 型の値を構成することはできなさそうです.
そういうわけで a -> b -> a という型の関数は,2つの選択肢の1つめを選択する関数ということになります.つまり,この型をもつ関数は const だということです.さて,2つの選択肢があるなら,2つめを選択する関数というのも考えましょう.これを const2 と呼ぶことにすれば,ナイーブには,

const2 :: a -> b -> b
const2 x y = y

でもちょっとこのような定義は,中身を見もしないのにパラメータの値に依存したかのような(実際にはもちろん引数の値に非依存),関数適用の値として定義する書き方は残念ですよね.関数は関数を結合して作りたいものです.というわけで直ぐに思いつくのは,引数の順序を交換するコンビネータ flip です.

const2 = flip const

選択肢が 2つあって,条件によってそのどちらかを選ぶということはプログラムではもっとも基本的な構成のひとつです.このような構成は if 式に表れます.

if c then t else e

という式は選択肢 t と e があって,c が True のとき t を,c が False のとき e を選択します.if 〜 then 〜 else 〜 は構文ですが,これは関数としても定義できます.すなわち,

if' c t e = if c then t else e

です.関数 if' の型は

if' :: Bool -> a -> a -> a

です.この型シグネチャ

if' :: Bool -> (a -> a -> a)

という意味であり,a -> a -> a は 2つの選択肢のなかから,どちらかを選ぶ関数にほかなりません.すなわち,if' は Bool値から対応する選択関数への変換であると考えられます.任意の型 a についてこの型シグネチャになりうる関数は,病的なものを除けば,const と const2 の2つしかありません.つまり,

if' True  = const
if' False = const id

というわけです.そこで,

type Boole = forall a. a -> a -> a
true :: Boole
true = const
false :: Boole
false = const id

などとすることもできるというわけです.これを見れば K コンビネータは真偽値に対応する重要な役割をもっていることがわかりますよね.ね.ね.:)

あれっ.const2 = flip const じゃなかったっけ?と思いませんでした.const2 = const id でもいいのですよ.それをたしかめるのは練習問題です.:p

Boole を使った FizzBuzz (追記: 2009-12-09)

いくつか,誤魔化しはあるけど f(^^;)

import Control.Applicative

true  = const
false = const id

if' True  = true
if' False = false

choice = flip . flip id
null'  = if' . null

fizzs = cycle $ take 3 $ "Fizz":repeat ""
buzzs = cycle $ take 5 $ "Buzz":repeat ""

fizzbuzz = zipWith fb [0..] (zipWith (++) fizzs buzzs)
  where fb = flip (<*>) null' . choice . show