この記事は (bouzuya) PureScript Advent Calendar 2016 の 8 日目です。 (bouzuya) PureScript Advent Calendar 2016 は bouzuya の PureScript 学習の記録です。
- ← 7 日目『 PureScript by Example 3 章の前半を読む - Qiita 』
- → 9 日目『 PureScript IDE (Visual Studio Code 拡張) の紹介 - Qiita 』
概要
昨日に続き PureScript by Example と PureScript Language Guide を見ながら進めていきます。今日は PureScript by Example の 3 章 の後半を読みます。
※注意事項: 英語と日本語訳では大きな差異があります。0.10.x
に対応している英語の側で進めます。
- PureScript by Example 2016-12-05 時点で 0.10.x 向け
- 実例による PureScript 2016-12-05 時点で 0.7 向け
- PureScript Language Guide 対象バージョン不明
PureScript by Example 3.6
type
キーワードで型を定義していきます。type
は型シノニム (type synonyms) を定義します。
Language Guide:
type Entry =
{ firstName :: String
, lastName :: String
, address :: Address
}
type Address =
{ street :: String
, city :: String
, state :: String
}
type AddressBook = List Entry
ここでは firstName
& lastName
& address
からなるレコードに Entry
という名前の型シノニムを定義しています。レコードがレコードを含んでいます。
Language Guide の例を見ると type Bar a = { foo :: a, bar :: a }
のような定義もできるようですね。へえ。
List Entry
と Array Entry
は別物ですって。そりゃあ、そうでしょうね。
PureScript by Example 3.7
List
は型コンストラクタ (type constructor) の例だそうです。名前のとおり型を構築するんでしょう。上記の AddressBook
で使った List Entry
は List a
が Entry
に適用されたもの。
種 (kind) の話ですね。
$ npm run pulp:psci
> chapter3@1.0.0 pulp:psci /Users/bouzuya/.ghq/github.com/paf31/purescript-book/exercises/chapter3
> pulp psci
impPSCi, version 0.10.2
Type :? for help
> import Data.List
> Nil :: List
Error found:
in module $PSCI
In a type-annotated expression x :: t, the type t must have kind *.
The error arises from the type
List
having the kind
* -> *
instead.
in value declaration it
See https://github.com/purescript/purescript/wiki/Error-Code-ExpectedType for more information,
or to contribute content related to this error.
> :kind Int
*
> :kind List
* -> *
> :kind List Int
*
関数適用みたいですね。List
は型引数をひとつとって型を返すわけですね。
Language Guide:
PureScript ではほかにも !
や #
があるんですね。また後で出てくるみたいです。
PureScript by Example 3.8
showEntry :: Entry -> String
showEntry entry = entry.lastName <> ", " <>
entry.firstName <> ": " <>
showAddress entry.address
型を書いてから、関数を書いていますね。実装は (<>)
で String
をくっつけているだけですね。
showAddress
も大差ないですね。スルー。
PureScript by Example 3.9
psci
で showAddress
と showEntry
をためしていますね。スルー。
$ npm run pulp:psci
> chapter3@1.0.0 pulp:psci /Users/bouzuya/.ghq/github.com/paf31/purescript-book/exercises/chapter3
> pulp psci
PSCi, version 0.10.2
Type :? for help
> import Data.AddressBook
> let address = { street: "123 Fake St.", city: "Faketown", state: "CA" }
> showAddress address
"123 Fake St., Faketown, CA"
> let entry = { firstName: "John", lastName: "Smith", address: address }
> showEntry entry
"Smith, John: 123 Fake St., Faketown, CA"
PureScript by Example 3.10
emptyBook :: AddressBook
emptyBook = empty
当然ですけど emptyBook
は空の List にするみたいです。
insertEntry :: Entry -> AddressBook -> AddressBook
insertEntry entry book = Cons entry book
psci
で Cons
の型を確認して、想定した型と一致することを確認していますね。
> import Data.List
> :type Cons
forall t1. t1 -> List t1 -> List t1
AddressBook
は List Entry
なので t1
を Entry
だと解釈すれば一致します。
PureScript by Example 3.11
何度かスルーしたカリー化ですね。PureScript の関数はカリー化された形であり、ひとつの引数しか取れません。複数の引数のようなものが必要な場合には、次の引数をとる関数を返します。
たとえば insertEntry
は Entry -> AddressBook -> AddressBook
なので、Entry
をとって AddressBook -> AddressBook
という関数を返します。返されたこの関数はさらに AddressBook
をとって AddressBook
を返します。型のとおりですし、関数の引数は常にひとつです。この例のように、関数の一部の引数だけを渡すことを部分適用と言います。
> let john = { firstName: "John", lastName: "Smith", address: address }
> let insertJohn = insertEntry john
insertJohn
は部分適用した結果、得られた関数です。john
を insert する関数です。 (そんなに john ばかり要らない気はしますが……)
PureScript by Example では加えてイータ変換に触れています。
insertEntry :: Entry -> PhoneBook -> PhoneBook
insertEntry entry book = Cons entry book
これは
insertEntry :: Entry -> PhoneBook -> PhoneBook
insertEntry = Cons
と同じ動きをします。このように同じ引数なので、わざわざ指定しなくても良いのです。
この形式には利点があるのでしょうが、個人的には引数を明示しているほうが読みやすいように感じます。型を見ないと何を取るのかもよく分かりませんし……。まだ慣れていないだけかもしれません。
PureScript by Example 3.12
List
のための関数を組み合わせて AddressBook
から Entry
を探す関数を定義するようです。
> import Data.List
> :t filter
forall a. (a -> Boolean) -> List a -> List a
> :t head
forall a. List a -> Maybe a
filter
はカリー化された 2 引数の関数ですね。第 1 引数の関数で第 2 引数の List
を filter して、その結果である List
を返すみたいですね。
head
は List
の先頭要素を返します。
Pursuit:
a
をそれぞれ AddressBook における要素に置き換えると次のようになります。
filter :: (Entry -> Boolean) -> AddressBook -> AddressBook
head :: AddressBook -> Maybe Entry
これらを使って findEntry
をつくります。
findEntry :: String -> String -> AddressBook -> Maybe Entry
findEntry firstName lastName book = head $ filter filterEntry book
where
filterEntry :: Entry -> Boolean
filterEntry entry = entry.firstName == firstName && entry.lastName == lastName
($)
や where
が初出なんですけど、概要としては filter
したものを head
しているだけです。
where
で補助関数として filterEntry
を定義しています。findEntry
の引数に渡された firstName
と lastName
に一致する Entry
かを Boolean
で返す関数ですね。
PureScript by Example 3.13
中置演算子 (infix operator) ・中置関数 (infix function) の話です。上記の ($)
に関連した話です。
Pursuit:
head $ filter filterEntry book
は head (filter filterEntry book)
と同じ動きをします。
($)
は apply
の alias で次のように定義されています。
apply :: forall a b. (a -> b) -> a -> b
apply f x = f x
infixr 0 apply as $
($)
は右結合の低い優先順位の演算子として定義されています。 apply
は第一引数の関数を第二引数の値に適用する関数です。
($)
を使えば括弧の入れ子を解消できます。
street (address (boss employee))
street $ address $ boss employee
なるほど、便利ですね。
ちなみに関数を演算子的に使うことも、その逆もできます。演算子を関数的に使う場合は ()
で括り、関数を演算子的に使う場合は ` で括れば良いです。
npm run pulp:psci
> chapter3@1.0.0 pulp:psci /Users/bouzuya/.ghq/github.com/paf31/purescript-book/exercises/chapter3
> pulp psci
PSCi, version 0.10.2
Type :? for help
> import Prelude ((+))
> 1 + 2
3
> (+) 1 2
3
> let f = (+)
> f 1 2
3
> 1 `f` 2
3
Language Guide:
- Functions as Operators - Language Guide
-
User-defined operators - Language Guide
[https://github.com/purescript/purescript/wiki/Language-Guide#functions-as-operators]
PureScript by Example 3.14
関数合成の話ですね。>>>
と <<<
です。それぞれ compose
と composeFlipped
の alias です。
Pursuit:
- (<<<) - Control.Semigroupoid - purescript-prelude - Pursuit
- (>>>) - Control.Semigroupoid - purescript-prelude - Pursuit
前述の findEntry
であれば次のように書き換えられます。
head $ filter filterEntry book
(head <<< filter filterEntry) book
(filter filterEntry >>> head) book
insertEntry = Cons
のように book
を削れば、findEntry firstName lastName = head <<< filter filterEntry
とできます。ふむふむ。
PureScript by Example 3.15
psci
でためしていますね。
Show
インスタンスでないと表示できない箇所は map showEntry entry
のようにして回避しているようです。古い分だと (<$>)
よりも読みかたの分かる map
になったということでしょうか。
Pursuit:
- map - Data.Functor - purescript-prelude - Pursuit
- (<$>) - Data.Functor - purescript-prelude - Pursuit
まとめ
3 章の後半は密度が高かったです。慌てて進めたので、雑になってしまいました。
参考
- Language Guide - Functions as Operators
- Language Guide - Kind System
- Language Guide - Type Synonyms
- Language Guide - User-defined operators
- Language Guide
- PureScript by Example - 3. Functions and Records
- PureScript by Example
- Pursuit - (<$>) - Data.Functor - purescript-prelude
- Pursuit - (<<<) - Control.Semigroupoid - purescript-prelude
- Pursuit - (>>>) - Control.Semigroupoid - purescript-prelude
- Pursuit - filter - Data.List - purescript-lists
- Pursuit - head - Data.List - purescript-lists
- Pursuit - map - Data.Functor - purescript-prelude
- Pursuit
- paf31/purescript-book
次回以降の TODO
- PureScript by Example & PureScript Language Guide を読み進める
- Tagged Unions / Newtype
psc-package
- PureScript IDE の導入
- 24 Days of PureScript, 2016
- 24 Days of PureScript, 2014
- 某氏の記事の紹介
- github.com/purescript のコードを読む
- github.com/purescript-node のコードを読む
- github.com/purescript-contrib のコードを読む