この記事はHaskell Advent Calendar 2014 の 13日目の記事です1。前日はarrowM さんの「Haskell rest フレームワークでREST API設計をサボる」、翌日はsolorabさんの「Haskellのlensの使い方」です。
HaskellでCocoaアプリ開発、したいですよね?したくない人は存在しないと仮定しても一般性を失わない筈なので、存在しないとして以下議論しましょう。というわけで、対象読者は、これから
Cocoa アプリ開発で Haskell を使いたいと思っている Haskell
プログラマです。Objective-C
(以下、Obj-C)がなんとなく読めればよりよいでしょうが、私じしんそこまで
Obj-C
は得意ではないので、まあ読めなくても何とかなるでしょう。また、本稿では
OS X 向けの Cocoa 開発を対象とし、iOS アプリの開発は扱いません2。また、以下で扱うlanguage-c-inline
は専ら
Obj-Cとの連携に注力されていますが、普通のC言語との交ぜ書きも同じ要領で出来る筈なので、OS
Xとか知らねーし!みたいな不逞の輩(!)な皆さんの参考にもなるのではないかと思います。
以下で採り上げる例は、全てGitHubで閲覧可能です。
Haskell で Cocoaアプリ開発?
その昔、HaskellでCocoaアプリ開発をするためのライブラリとして、HOCというものがありました。これはCocoa APIに対する包括的なラッパーライブラリを提供することを企図したものでしたが、現在ではもうメンテナンスされておらず、Haskell / Obj-C 双方の変化に追随出来ていません。
今回以下で紹介するのはlanguage-c-inline
を用いる方法です。名前に
inline
と入っている事からも推察出来るように、今回採る方法は巨大なラッパライブラリを利用するのではなく、Haskellのプログラムの中に
Obj-C のコードを交ぜ書きするスタイルです。
ですので、Obj-Cを自分で書かなければならないという点ではラッパライブラリを用いるのに較べて少し手間かもしれません。しかし、ラッパを使うにしてもちゃんとしたプログラムを書くには、CocoaのAPIリファレンスを読まなくてはいけない訳ですし、さして必要な労力は変わらないでしょう。
また、language-c-inline
じたいは、HOCが提供していたような、Obj-Cのオブジェクト・システムを再現するための機構は提供していません。しかし、GHC
の最近の型機能を使えば、その必要な部分だけをエミュレートするような型システムを簡単に設計することが出来ます。そもそも、Obj-CのAPIを呼んだり、コントローラを書いたりする部分以外は関数型のパラダイムを使ってプログラミングする訳ですから、Obj-Cが提供するような高度なオブジェクト・システムすべてが使える必要はなく、単純な継承とアップキャスト、(unsafe
な)ダウンキャスト、id
型くらいがあれば十分な訳です。以下ではその技法も含めて解説出来ればな、という感じです。
また、オブジェクト・システムを模倣する上で、現在 Hackage
に上がっている
language-c-inline
(0.7
系統)には無い機能を使っています。なので、以下の作業をする上ではGitHubから直接最新版を取ってくるのが一番やりやすいと思います。そのうち最新版がリリースされる筈ですが、作者の方が忙しいので、Hackage
に上がるのはもう暫く待ったほうが良いようです。
開発チュートリアル
Hello, World!
色々と能書きを述べてきましたが、まあ取り敢えず Hello, World をやってみましょう。
{-# LANGUAGE QuasiQuotes, TemplateHaskell #-}
module Main where
import Language.C.Inline.ObjC
import Language.C.Quote.ObjC
"<Foundation/Foundation.h>"]
objc_import [
nsLog :: String -> IO ()
= $(objc ['msg :> ''String] $
nsLog msg
void [cexp| NSLog(@"%@", msg) |])
objc_emit
= do
main
objc_initialise"Hello, from Haskell-Cocoa!" nsLog
一つずつ解説していきましょう。まず四行目の objc_import
は Template Haskell
マクロで、裏で生成されるヘッダファイル等が import
するファイルを指定しています。今回は単にCocoaの機能を使って文字列を出力したいだけなので、Foundation
を読み込ませています。
続く nsLog
の部分では、objc
マクロが呼ばれています。これは、引数の名前とその型のヒントのリスト、返値のアノテーションが付いた定義部を取って、しかるべきCラッパ関数と
Haskell の FFI
宣言を生成するマクロです。ここでは、次の形のラッパ関数が定義されています:
String
型の引数msg
を取り、- 定義が
NSLog("%@", msg)
であり、 - 返値は
void
であるような C 関数。
しかし、ここで生成されているのはあくまで「定義」であって、FFI
宣言が実際にスプライスされたり、関数がヘッダファイルや.m
ファイルとして書き出されるのは次の行の
objc_emit
が呼ばれた段階です。逆に云えば、いくら objc
マクロなどを使って Obj-C コードを埋め込んでも、objc_emit
が呼び出されていなければそれらが機能することはないので気を付けましょう。
その後の main
関数の所では、まず
objc_initialise
を呼んで上で定義した FFI や
nsLog
がちゃんと機能するような前処理を行っています。その後、nsLog
を呼んでログを出力してめでたしめでたしという訳です。
では、これで動くかどうか実際にコンパイル&実行してみましょう。hello.hs
などという名前で保存されているとすると、これをコンパイルするには、次のようにします:
$ ghc -c hello.hs
$ cc -fobjc-arc -I/Library/Frameworks/GHC.framework/Versions/7.8.3-x86_64/usr/lib/ghc-7.8.3/include -I/Library/Frameworks/GHC.framework/Versions/7.8.3-x86_64/usr/lib/ghc-7.8.3/../../includes -c -o hello_objc.o hello_objc.m
$ ghc -o hello hello.o hello_objc.o -package language-c-quote -package language-c-inline -framework Foundation
$ ./hello
2014-12-13 00:00:00.000 hello[88135:507] Hello, from Haskell-Cocoa!
気をつけるべき所は、まずは普段通りに ghc (--make)
を呼ぶのではなく、ghc -c
を呼んでオブジェクトファイルを生成するに留めておくことです。これは、最初にghc
を呼んだ段階では未だ
Obj-C
ヘッダファイルや、それに付随するオブジェクトファイルが生成されていないので、リンクしようと思っても出来ないからです。なので、一旦
ghc -c
を走らせて Template Haskell の処理を行わせて、Obj-C
ヘッダ・ソースファイルを生成させているのです。実際、この後にディレクトリの内容を確認してみると、以下の四つのファイルが増えていることがわかります:
hello_objc.h hello_objc.m hello.hi hello.o
名前からもわかるとおり、この内で *_objc.[hm]
という名前の物が今回生成された Obj-C
ファイルです。このように、生成されるファイルは
元のファイル名_objc.[hm]
という名前で生成されるので、これと被るようなファイル名は使わないようにしましょう(また、どういうタイミングで生成されるのかもよくわからないのですが、元のファイル名_stub.[hm]
という名前のヘッダファイルが出力されることもあります)。バージョン名は場合によっては異なることもあると思うので、適宜修正してください。たぶん
locate HsFFI.h
とかやれば見付かる筈です。
続いて、今回生成されたObj-Cファイルをコンパイルしてやる必要があります。そこでcc
コマンド(実際にはclang
?)を呼び出してリンクしている訳です。ここで、-I/Library/Frameworks/GHC.framework/Versions/7.8.3-x86_64
とか
-I/Library/Frameworks/GHC.framework/Versions/7.8.3-x86_64/usr/lib/ghc-7.8.3/../../includes
といったオプションが指定されているのは、Haskell の FFI
で値をやり取りするのに必要なヘッダファイル(HsFFI.h
)を見付ける為です。
最後に、今までに生成したオブジェクトファイルをリンクして、実行ファイルを作成します。その際には
Haskell 側で使っているパッケージを -package
オプションで、Obj-C 側で使っているフレームワーク(今回の場合は
Foundation
)を -framework
オプションでそれぞれ指定してやる必要があります。
再コンパイルに御用心
生成される Obj-C ファイルで使われるラッパ関数名は、Template Haskell
が走る度に異なる名前になります。ファイルの幾つかを変更して再コンパイルすると、「関数が見付からないよ!」と怒られる場合があります。その場合は、生成された
.hi
, .o
, _objc.[hm]
ファイルをすべて削除して、もういちど最初からやり直してみてください。
Currency Converter – オブジェクト指向界の Hello, World! を Haskell で
はじめの一歩:原始的な GUI
Hello, World! はうまくいきましたね。それではもう少しマトモなアプリケーションを書いてみましょう。OOPにおける Hello, World! とでもいうべき Currency Converter (通貨換算器)を作ってみましょう。
まず、Xcodeを開いて新しい “Cocoa Application”
プロジェクトを作成します(そんなの面倒くさい!とか Xcode
の使い方なんて知らん!という場合、こちらに既に用意してあります!)。MainMenu.xib
を開いて、次のような感じでウインドウにコントロールを配置し、AppDelegate
との間にアウトレット、アクションを設定してください:
ロジックなどはまだ実装していませんが、この段階でいちど
Release
をターゲットにしてアプリケーションをビルドします。ビルドが完了したら、出来上がった実行ファイルを適当な場所にコピーしておいてください。
続いて、Haskell でアプリケーションのロジックを記述します。
Main.hs
ファイルを作成して、必要なモジュールやObj-C側のフレームワークの読み込みをしておきます:
{-# LANGUAGE DeriveDataTypeable, QuasiQuotes, TemplateHaskell #-}
module Main where
import Data.Typeable (Typeable)
import Language.C.Inline.ObjC
import Language.C.Quote.ObjC
"<Cocoa/Cocoa.h>"] objc_import [
では、メインロジックである変換関数convert
を実装してみましょう。
通貨換算器という大仰な名前でも、結局やることは
- 換算前の価格(
dollarsField
)と為替レート(rateField
)の値を取得し、 - 両者を掛け算して、
- その結果を表示(
resultField
)する。
ということだけで、実質掛け算するだけです。Cocoa
のリファレンスマニュアルなどと首っぴきになれば、テキストフィールドの値の取得・設定は-(int) intValue
や-(double) doubleValue
、-(void) setIntValue:(double) val
などを使えばよさそうなので、メインロジックであるconvert
の実装は次のように書けば良さそうです:
convert :: AppDelegate -> IO ()
= do
convert app <- intValue =<< dollarsField app
input <- doubleValue =<< rateField app
rate floor $ fromIntegral input * rate) =<< resultField app setIntValue (
このあとすべき事が幾つかあります:
AppDelegate
やNSTextField
などに対応する型をHaskell側で定義するintValue
,doubleValue
,setIntValue
の実装- Obj-C 側の
AppDelegate
クラスのconvert:
アクションと上で実装したconvert
関数との紐付けとmain
関数の実装 - アプリケーションバンドルの作成
それぞれ順に見ていきましょう。
Obj-C のオブジェクトを Haskell で扱うには?
language-c-inline
では、基本型(int
, BOOL
, char
など)以外のObj-Cの値は、ForeignPtr
をnewtype
で包んだ型で表現されます。たとえば、AppDelegate
に対応するHaskellの型は、
newtype AppDelegate = AppDelegate (ForeignPtr AppDelegate)
deriving (Typeable)
のように宣言されます。型コンストラクタの名前が、そのままObj-C
での型名として扱われます。現時点におけるlanguage-c-inline
ライブラリの内部実装では動的キャストを使っているため、Typeable
のインスタンスを導出させておく必要があります。また、これも内部実装の制約から、newtype
宣言はレコード型ではなく、このようなコンストラクタのみの形で宣言される必要があることに注意してください。
実際には、AppDelegate
の値をやりとりするには、次のような行を追加しておく必要があります:
marshalAppDel :: AppDelegate -> IO AppDelegate
= return
marshalAppDel
objc_marshaller 'marshalAppDel 'marshalAppDel
字面からなんとなくわかるかもしれませんが、ここではAppDelegate
型の値をHaskellとObj-Cの間でやりとりする際のマーシャリング関数を定義しています。objc_marshaller
マクロは、それぞれA -> IO B
と B -> IO A
という形の型を持つ関数の名前をとって、A
,
B
型の値を相互変換するための情報を保存します。このマーシャリングの情報はモジュールごとに保存されるので、モジュールを変えるたびにobjc_marshaller
を呼び出して関数を登録してやる必要があることに注意してください。
今回の場合は、単にポインタに包まれた値をやりとりするだけなので、A = B = AppDelegate
として何もせずにreturn
するだけの関数を登録しています。
同様にして、NSTextField
を表す型も定義できます:
newtype NSTextField = NSTextField (ForeignPtr NSTextField)
deriving (Typeable)
marshalNSTextField :: NSTextField -> IO NSTextField
= return
marshalNSTextField
objc_marshaller 'marshalNSTextField 'marshalNSTextField
定義順に御用心
ここで、一つ注意があります。こうしたラッパ型や、次の節で定義するラッパ関数は、それを呼び出す関数の定義より前にもってくる必要があります。Haskellでは通常関数の定義順に注意する必要はありませんが、language-c-inline
ではTemplateHaskellのマクロをふんだんに使っているので、マクロが走る時点において型・関数の情報が必要となるため、このような制約があります。
ラッパ関数の定義
intValue
やsetIntValue
、あるいはdollarsField
アウトレットなどを取得するためのHaskell関数は、Hello,
World!の例でNSLog
のラッパ関数を定義した時と同様にして、objc
マクロを使って定義してやれば十分です。ためしにintValue
,
rateField
,
setIntValue
の定義を見てみましょう:
intValue :: NSTextField -> IO Int
= $(objc ['txt :> ''NSTextField] $
intValue txt 'Int <: [cexp| [txt intValue] |])
'
rateField :: AppDelegate -> IO NSTextField
= $(objc ['app :> ''AppDelegate] $
rateField app Class ''NSTextField <: [cexp| app.rateField |])
setIntValue :: Int -> NSTextField -> IO ()
=
setIntValue i txt $(objc ['i :> ''Int, 'txt :> ''NSTextField] $
void [cexp| [txt setIntValue: i] |])
ここで、Class
という見慣れないコンストラクタが出現しています。そもそも、(:>)
や(<:)
の引数として異なる型の値が渡されているように見えます。これはどういうことなのでしょう?
というところで、(:>)
やClass
の定義を見てみましょう:
data Annotated e where
(:>) :: Hint hint => e -> hint -> Annotated e
Typed :: Name -> Annotated Name
(<:) :: Hint hint => hint -> e -> Annotated e
<:) = flip (:>)
(
data Class where
Class :: IsType t => t -> Class
まず、(:>)
はどうやらAnnotated
型のコンストラクタだったようです。Annotated
型は、その名の通り値を型の相互変換に関するヒントで注釈したものです。そのヒントとなりうる型はライブラリで定義されていて変更出来ませんが、TypeQ
, Name
, Class
型などがあります。Class
型は、ForeignPtr
で包まれたObj-Cのオブジェクトである、という意味のヒントのようですね。
こうしたことを踏まえて上の定義を見てみれば、あとはそんなに驚くようなことはやっていないと思います。
これらに習って、doubleValue
やdollarField
,
resultField
など必要な関数のラッパを定義してみてください。
アクションと Haskell 関数の紐付け、およびmain
関数
以上で、Haskell側のconvert
関数の実装は完了しました。これを
Obj-C 側から呼び出すにはどうすればいいでしょうか?
まず、Obj-Cのヘッダファイルの情報を生成してやる必要があります。
そこで、次のコードを、AppDelegate
のマーシャリング関数を定義する行より前に追加してください:
objc_interface [cunit|
@interface AppDelegate : NSObject <NSApplicationDelegate> @property (weak) typename NSTextField *dollarsField; @property (weak) typename NSTextField *rateField; @property (weak) typename NSTextField *resultField; - (void)convert:(id)sender; @end
|]
幾つか説明が必要でしょう。まず、これをobjc_marshal
より前に持ってくる必要があるのは、生成する対象のObj-C(というかC言語)が関数・型の定義順にうるさい言語だからで、objc_marshall
が生成するマーシャル関数のプロトタイプ宣言にAppDelegate
という型名があるため、それより前でAppDelegate
クラスを定義しておく必要があるからです。
また、Obj-Cではこんな変な市にtypename
などというキーワードが来ることはなかった筈です。このキーワードは、languace-c-quote
独自のもので、Cのコードをパーズするのにあらかじめ型の情報が必要となるため用意されています。int
やdouble
, char
などの基本型はlanguage-c-quote
も知っているので大丈夫ですが、NSTextField
は知りせん。なので、「この後に型名がくるよ!」という事を明示するために、typename
キーワードでそれを明示している訳です。typename
キーワードが必要になるのは、今回のような変数・プロパティの宣言の他に、関数の引数や返値の型宣言などの部分でも必要になります。
さて、これでヘッダファイルの情報は十分です。実際にconvert:
アクションからHaskellのconvert
関数を呼び出すようにするには、次のコードを書く必要があります:
objc_implementation [Typed 'convert] [cunit|
@implementation AppDelegate - (void) convert: (id)sender { convert(self); } @end
|]
このコードは、convert
関数が定義されている行よりも下に書いてください。これも、TemplateHaskellの制限によるものです。
objc_implementation
マクロは、その名の通り実際のObj-Cの実装ファイル(.m
)のトップレベル宣言を定義するマクロです。
objc_interface
マクロはObj-Cの式だけを取りましたが、objc_implementation
マクロは、実装内部で使われるHaskell関数のヒント付き名前の一覧も引数に取ります。ここでは[Typed 'convert]
がそれに当ります。Typed
は、上でも出て来たAnnotated
型のコンストラクタで、「この名前の関数は定義済みだから適当に型推論しといて」という意味です。同じ名前のconvert
が出て来て混乱するかもしれませんが、-(void) convert:(id)sender
の定義部で呼ばれているconvert
は、Haskellにおけるconvert
関数を指しています。実際には、この裏で一意な名前を持つラッパ関数の呼び出しに置き換えられますが、ナイーヴにはconvert
Haskell関数を呼び出していると思えばよいです。
最後に main
関数を実装しましょう。以下の行をファイルの最後に追加してください:
nsApplicationMain :: IO ()
= $(objc [] $ void [cexp| NSApplicationMain(0, NULL) |])
nsApplicationMain
objc_emit
main :: IO ()
= do
main
objc_initialise nsApplicationMain
まず、objc_initialise
を呼び出してObj-Cラッパーを初期化した後、おもむろに
NSApplicationMain
関数を呼び出して、.xib
ファイルからオブジェクトを初期化したりといったCocoaの側の初期化をよしなにやってもらっています。
これでMain.hs
の内容は完成です(完全なソースコードはこちら)。
アプリケーションバンドルの作成
では、アプリケーションバンドルを作成しましょう。まず、Main.hs
を今まで通りコンパイルします:
$ ghc -c Main.hs
$ cc -fobjc-arc -I/Library/Frameworks/GHC.framework/Versions/7.8.3-x86_64/usr/lib/ghc-7.8.3/include -I/Library/Frameworks/GHC.framework/Versions/7.8.3-x86_64/usr/lib/ghc-7.8.3/../../includes -c -o Main_objc.o Main_objc.m
$ ghc -o CurrencyConverter Main.o Main_objc.o -package language-c-quote -package language-c-inline -framework Cocoa
問題なくコンパイルできていれば、CurrencyConverter
バイナリが出来ている筈です。そこで、これを一番最初にXcodeで作成しておいた
CurrencyConverter.app
のしかるべき場所にコピーします(CurrencyConverter.app
は同じディレクトリに配置されていると仮定します):
$ cp CurrencyConverter CurrencyConverter.app/Contents/MacOS/CurrencyConverter
これで完了です!早速起動してみましょう:
値を入力して、Convert
ボタンを押せば、ちゃんと期待通りの動作をすることが確認できました。やったね!
寄り道: オブジェクトシステムを模倣する
今までは各クラス毎に対応する newtype
を宣言して、それらを使った型を引数に取るようなラッパ関数を使っていました。
通貨変換器のような簡単なものであればそれで十分ですが、この方針ではクラスの継承関係などが絡んでくるとサブクラスごとに異なるラッパ関数を定義することになり不便です。
そこで、本節では少し寄り道して、GHCの型システムを使って軽量なオブジェクトシステムを構築してみることにします。 以下で実装する簡易オブジェクトシステムはGitHubにアップされています。
ロジックはすべてHaskellで書くとすれば、何もObj-Cのオブジェクトシステムを完全に模倣する必要はなさそうです。 要は、Obj-C のAPIを呼び出す時に継承を考慮してアップキャストが出来れば十分なので、単純な継承関係とメッセージをコード出来ればよいでしょう。
継承関係とインスタンスの表現
まずクラス間の継承関係ですが、これは次のように型クラスで表現するのが自然でしょう:
{-# LANGUAGE PolyKinds, TypeOperators, MultiParamTypeClasses #-}
class (super :: k) :> (sub :: k2)
instance a :> a
要は、型レベルの不等号(:>)
によってそのまま継承関係を定義してやる訳です。Obj-Cには通常の継承関係の他にも多重継承にあたるようなプロトコルの機能がありますが、それについても適当に
(:>)
でコードしてやれば呼び出す分には問題ないでしょう。
では、Obj-Cでのオブジェクトの継承関係をこれを使って実際に表現していくことにしましょう。取り敢えず、klass
クラスのインスタンスを表す型として、Object
型を定義しましょう:
newtype Object (klass :: k) = Object (ForeignPtr ())
deriving (Typeable, Show, Eq, Ord)
ForeignPtr
を包む
newtype
は、Haskellにおける型名と同じ名前のクラスと同一視されるのでした。そこで、たとえば
NSString
を表すためには
type NSString = Object "NSString"
のように Object
の型パラメータを埋めた型シノニムを定義して、それを用いることにします。これで巧くいくためには、GitHub上の最新のlanguage-c-inline
が必要です。これは、型シノニムの名前を使ってObj-C側のクラス名を解決しつつ、ForeignPtr
に行き着くまで型を展開する、という挙動が、Hackageに上がっている版にはまだ実装されていないからです。
また型の文脈に文字列リテラルのようなものが出て来てびっくりするかもしれませんが、これはSymbol
カインドという型レベルの文字列のようなものです。Symbol
を使うようにすると、一々クラス名を表すダミーの型を定義せずに済むので楽です。
このようにすると、アップキャストは単にコンストラクタを付け替えるだけで出来るようになります:
cast :: (a :> b) => Object b -> Object a
Object ptr) = Object ptr cast (
ダウンキャストも同様に実装出来ますが、まあ安全ではないので
unsafeDownCast
とかいう名前にしておきましょう。
メッセージの表現
Obj-Cはオブジェクト指向言語の中でも、Smalltalkに大きな影響を受けたメッセージベースのオブジェクトシステムを採用しています。 そこで、次はオブジェクトに対するメッセージを扱えるようにしましょう。Obj-C ではメッセージの名前をセレクタと呼ぶので、セレクタを表す型クラスを定義してみましょう:
class Selector cls msg | msg -> cls where
data Message (msg :: k') :: *
type Returns msg :: *
send' :: Object cls -> Message msg -> Returns msg
いまいち意味が見えづらいかもしれません。たとえば、NSControl
クラスの-(void) setObjectValue:(id) obj
に当るセレクタは次のように定義します:
instance Selector "NSControl" "setObjectValue" where
data Message "setObjectValue" = SetIntValue NSObject
type Returns "setObjectValue" = IO ()
SetObjectValue num) = $(objc ['ctrl :> Class ''NSControl, 'obj :> Class ''NSObject] $
send' ctrl ( void [cexp| [ctrl setObjectValue: obj] |])
つまり、Selector
の関連データ型である
Message
はメッセージを送るのに必要な引数を保持するデータ型であり、Return
関連型はその操作の結果のHaskellにおける返値の型に対応している訳です。書きやすいように、中置演算子版も用意しましょう:
infixl 4 #
(#) :: (a :> b, Selector a msg, Returns msg ~ IO c, MonadIO m) => Object b -> Message msg -> m c
# f = liftIO $ send' obj f
obj
infixl 4 #.
(#.) :: (a :> b, Selector a msg, Returns msg ~ IO c, MonadIO m) => IO (Object b) -> Message msg -> m c
#. sel = liftIO $ recv >>= flip send' sel recv
(#)
が中置演算子版で、のものは、メソッドチェーンにして呼び出していくときに、便利なように定義されたものです。
継承ツリーをちゃんと機能させるために
ところで、今までに定義してきた継承関係の推論規則には、反射律(A は A 自身のサブクラス)しか入っていません。このままだと、サブクラスのサブクラスが元のスーパークラスのサブになりません3。では、推移律を条件に入れてはどうでしょうか?
instance (a :> b, b :> c) => a :> c
残念ながら、これはうまくいきません。a :> c
のインスタンスを得るのに、間に出て来る
b
をどう扱えば良いかわからないからです。
これには、たとえば、単純な継承関係をやめて、例えば継承関係を一段ずつ型レベルリストにして型パラメータで持たせるといった方法が考えられます。しかし、なんだかそれは余りにもオーバーキルです。他にも型機能を駆使して実現する方法もあるかもしれませんが、ここでは違う方針を取ります。
どうするのかというと、直接 (:>)
のインスタンスを宣言するのではなく、それまでの型情報を使って、スーパークラスのスーパークラス、更にそのスーパークラス……と辿っていって、推移律を満たすのに必要なインスタンスを宣言するような
Template Haskell マクロを実装してしまうことにします。
という訳でそのように実装されているのが、defineClass
およびdefinePhantomClass
マクロです(それぞれMessaging.Macroで定義されています)。たとえば、NSObject
, NSString
, NSMutableString
などの継承関係を定義するには、次のようにすれば、万事面倒を見てくれます:
"NSObject" Nothing
defineClass "NSString" $ Just ''NSObject
defineClass "NSMutableString" $ Just ''NSString defineClass
つまり、NSObject :> NSString
,
NSString :> NSMutableString
,
NSObject :> NSMutableString
といった、必要な(:>)
のインスタンスがこれにより全て生成され、type NSObject = Object "NSObject"
などの型シノニムも定義されます。
definePhantomClass
は、更に幽霊型を引数にもつように定義してくれます。たとえば、
1 "NSArray" $ Just ''NSObject definePhantomClass
のようにすると、type NSArray a = Object "NSArray"
といったような、型シノニムが宣言されるのです。ただ、これは今のところ共変性や反変性とかを全く考慮していないので、あんまり嬉しくないかもしれません。
これで必要な継承関係は面倒を見てくれるようになりましたが、このままだと折角継承関係を定義したのに、手動でアップキャストしたりしながらメッセージを送らないといけません。そこで、まずは受け手を自動でアップキャストしてくれるような
send
を実装しましょう:
send :: (a :> b, Selector a msg) => Object b -> Message msg -> Returns msg
= send' . cast send
単にcast
を前に合成してやればいいだけですね。問題は、メッセージの引数のアップキャストですが、各メッセージに対してたとえば以下のようにしてObject
型の引数を取る引数にcast
を被せた便利関数を定義すればよさそうです:
setObjectValue :: ("NSObject" :> a) => a -> Message "setObjectValue"
= SetObjectValue (cast obj) setObjectValue obj
こうしたものを一々定義するのは面倒なので、これもマクロで解決してしまいましょう。ということで、造られたのが
defineSelector関数です。これを使うと、たとえばsetObjectValue
の定義は次のようになります:
= "setObjectValue"
defineSelector newSelector { selector = (''NSControl, "ctrl")
, reciever = ["obj" :>>: Class ''NSObject]
, arguments = [cexp| [ctrl setObject: obj] |]
, definition }
:>>:
は、language-c-inline
の:>:
を模倣して定義した型です。模倣したのは、language-c-inline
のHint
回りのインスタンス等を後付けで増やせなかったからです。多分。また、Name
ではなくString
型の値を取るようになっていることに注意してください。
ここまでやれば、だいたい Haskell から呼び出す分の Obj-C のオブジェクトシステムは模倣出来ることになるでしょう。
Symbol
型の利用について
上の説明では簡単のため Symbol
カインドの型で Obj-C
のクラス名を表しましたが、language-c-inline
の制約上、現れる型はすべてTypeable
のインスタンスである必要があります。GHC
7.8系ではSymbol
カインドの型はTypeable
のインスタンスになっていないので、GitHubに上がっている版では、data NSObjectClass
のように定義部を持たないダミーの型をよういして、それをDataKinds
で型レベルに持ち上げて
type NSObject = Object NSObjectClass
と定義することで回避しています。
他方、セレクタについてはマーシャリングの時に出て来ないので、Symbol
カインドの型レベル文字列を使って表しています。
GHC 7.10 からは型レベル文字列や型レベル数値も Typeable
のインスタンスになるので、素直に両方共 Symbol
を使った実装にできる予定です。
Currency Converter をもう少しそれっぽくする – FRP との出会い
閑話休題。話を Currency Converter に戻しましょう。
取り敢えず動くものはできましたが、人間というのは怠惰なもので(Haskellerならなおさらlazy!)、「Convertボタン押すの面倒くさいなあ」と思います。思いますよね。
そこで、テキストフィールドの値が変化したら、リアルタイムで再計算されるようにしてみましょう。Objective-Cではこういう場合、Cocoa
Bindingsを使うのが定石ですが、ここでは Haskell の FRP
ライブラリを使って同様のことを実現してみます4。具体的には、AppDelegate
クラスに対して、NSTextField
のデリゲートの役割も担わせて、変更があるたびに再計算させてやれ、という戦略です。
また、上で折角オブジェクトシステムを構築したので、これを使って書き直すこともやってみましょう。
準備とディレクトリ構成の変更
ところで、いちいちghc
とかcc
を適切な順番で呼ぶのって面倒ですよね。フレームワークや必要なパッケージの情報は
Cabal
ファイルにまとめておいて、あとは適宜必要なモジュールや生成されたObj-Cファイルなどを再帰的にコンパイルしてるえたら便利です。
という訳で、コーディングに入る前にここをまず自動化してしまうことにします。まず、以下のようなディレクトリ構成にします:
+- src
| |
| +- Main.hs
| |
| +- AppDelegate.hs
| |
| +- Messaging.hs
| |
| +- Messaging/Core.hs, Messaging/Macros.hs
|
+- xcodeproj
| |
| +- CurrencyConverter
| |
| +- CurrencyConverter.xcodeproj
| |
| +- CurrencyConverter
| |
| +- build
| |
| ⋮
|
+- CurrencyConverter.cabal
|
+- Builder.hs
CurrencyConverter.cabalとBuilder.hsはそれぞれ左のリンクから取得しておいてください。また、Messaging
関連はobjc-messagingのレポジトリからダウンロードして、src
に放り込んでおいてください。
Builder.hs
を動かすには、shakeビルドシステムが必要なので、cabal install shake
としておいてください。この記事はShakeの入門記事ではないので、Builder.hs
の内容についてはとくに立ち入らないことにします。
はじめの一歩
ツールの整備を強引に終わらせたので、まずは次のように.xib
ファイルを変更しましょう:
今回は main
関数の部分と、AppDelegate
の実装を分離することにします。上のディレクトリ構成図通りsrc
ディレクトリにMain.hs
とAppDelegate.hs
を作成して、Main.hs
の内容を以下のように簡略化しておきます:
{-# LANGUAGE DeriveDataTypeable, QuasiQuotes, TemplateHaskell #-}
module Main where
import Data.Typeable (Typeable)
import Language.C.Inline.ObjC
import Language.C.Quote.ObjC
import qualified AppDelegate as Delegate
"<Cocoa/Cocoa.h>"]
objc_import [
nsApplicationMain :: IO ()
= $(objc [] $ void [cexp| NSApplicationMain(0, NULL) |])
nsApplicationMain
objc_emit
main :: IO ()
= do
main
objc_initialise
Delegate.objc_initialise nsApplicationMain
Haskell側の初期化はモジュール毎におこなう必要があるため、main
関数内でAppDelegate
モジュールのobjc_initialise
を呼んでやる必要のあることに注意してください。
さて、これからAppDelegate.hs
にメインのロジックを記述していく訳ですが、取り敢えず今はダミーのコードを書いておきましょう:
{-# LANGUAGE DeriveDataTypeable, FlexibleInstances, MultiParamTypeClasses #-} {-# LANGUAGE QuasiQuotes, TemplateHaskell, TypeFamilies #-} module AppDelegate where import Data.Typeable (Typeable) import Language.C.Inline.ObjC import Language.C.Quote.ObjC objc_import ["<Cocoa/Cocoa.h>"] newtype AppDelegate = AppDelegate (ForeignPtr AppDelegate) deriving (Typeable) marshalAppDel :: AppDelegate -> IO AppDelegate marshalAppDel = return objc_marshaller 'marshalAppDel 'marshalAppDel nsLog :: String -> IO () nsLog str = $(objc ['str :> ''String] $ void [cexp| NSLog(@"%@", str) |] ) changed :: AppDelegate -> IO () changed app = nsLog "dummy!" objc_interface [cunit|
@interface AppDelegate : NSResponder <NSApplicationDelegate, NSControlTextEditingDelegate> @property (weak) typename NSTextField *dollarsField; @property (weak) typename NSTextField *rateField; @property (weak) typename NSTextField *resultField; - (void)controlTextDidChange:(typename NSNotification *)obj; @end
|] objc_implementation [Typed 'changed] [cunit|
@implementation AppDelegate - (void) controlTextDidChange:(typename NSNotification*) aNotification { changed(self); } @end
|] objc_emit
コンパイルしてみます。
$ runhaskell ./Builder.hs
簡単!起動してみると、まあ単に何も起きないウィンドウ一枚だけのアプリになってます。それでもConsole.appでログを見てみると、入力するたびに
controlTextDidChange:
のログが残っていることがわかるでしょう。
初期化・更新ロジックの実装
宣言通り、FRPを使ってメインロジックを実装します。特に、今回はsodiumを使うので、適宜インストールしておいてください。
まずは、初期化処理を書きましょう。現在の状態を保存するためのSession
型を定義し、それを誂える為の関数
newSession
を定義します:
type Dollars = Int
type Rate = Double
data Session = Session { pushDollars :: Dollars -> IO ()
pushRate :: Rate -> IO ()
, application :: AppDelegate
,deriving (Typeable)
}
newSession :: AppDelegate -> IO Session
= sync $ do
newSession app <- newBehaviour 0
(dolBh, dolL) <- newBehaviour 0
(ratBh, ratL) <- listen (value $ (*) <$> dolBh <*> ratBh) $ \val ->
_ # resultField #. setIntValue (floor val)
app return $ Session (sync . dolL . fromIntegral) (sync . ratL) app
まず、内部状態を表すSession
型はドル欄とレート欄の変化を通知する関数pushDollars
およびpushRate
、そして現在走っているAppDelegate
の三つ組として定義しています。フィールド内容の変更が通知されたら、これらの関数を通してFRPエンジン側に変更を伝達する訳です。
そうした通知関数を作成しているのが newSession
関数です。FRPにおいてはBehaviorというのは連続的に変化する値を表します。newSession
の二、三行目で、それぞれドル値とレート値を表現するBehaivourと変更通知関数を生成しています。
それらの変更があった時に実際に結果を更新する設定をしているのが、listen
を呼んでいる続く二行です。(#)
や(#.)
は先程定義したメッセージ送信用の中置演算子です。ここで使われているメッセージやクラスは次のように定義されています:
"NSObject" Nothing
defineClass
objc_interface [cunit|
@interface AppDelegate : NSResponder <NSApplicationDelegate, NSControlTextEditingDelegate>
@property (weak) typename NSTextField *dollarsField;
@property (weak) typename NSTextField *rateField;
@property (weak) typename NSTextField *resultField;
- (void)controlTextDidChange:(typename NSNotification *)obj;
@end
|]
"AppDelegate" (Just ''NSObject)
defineClass 'AppDelegate
idMarshaller '
"NSTextField" (Just ''NSObject)
defineClass 'NSTextField
idMarshaller '
= "setIntValue"
defineSelector newSelector { selector = (''NSTextField, "field")
, reciever = ["num" :>>: ''Int]
, arguments = [cexp| [field setIntValue: num] |]
, definition
}= "resultField"
defineSelector newSelector { selector = (''AppDelegate, "app")
, reciever = [cexp| [app resultField] |]
, definition = Just [t| NSTextField |]
, returnType }
ここで idMarshaller
という関数が呼びだされています。これは先程説明したオブジェクトシステム生成用のマクロについてくるオマケみたいなもので、一々return
の型を詳しくしただけのマーシャリング関数を定義しなくても、勝手に良い感じにしてくれるようにしたものです。
厳密には、defineClass
の段階でreturn
の型を特価させただけのマーシャリング関数が生成され、idMarshaller
が呼ばれるることで、それを実際にマーシャリング用の辞書に登録しています。どうせならdeinfeClass
呼んだだけで登録するまでやってくれれば良い気がしますが、これはlanguage-c-inline
の現在の内部実装の都合上分けざるを得ないのです。
さて、これで初期化ロジックは出来たので、これを Obj-C
から呼び出すようにします。objc_implemenatation
の部分を次のように書き換えてください:
objc_implementation [Typed 'changed, Typed 'newSession] [cunit|
@interface AppDelegate () @property (assign) typename HsStablePtr session; @end @implementation AppDelegate - (void)applicationDidFinishLaunching:(typename NSNotification *)aNotification { self.session = newSession(self); } - (void) controlTextDidChange:(typename NSNotification*) aNotification { changed(self); } @end
|]
AppDelegate
に内部状態を保持するsession
変数を追加して、アプリケーションの起動が完了した時点で先程のnewSession
関数を呼び、初期化された内部状態をそこに代入するようにしています。そんなに真新しいことはやっていませんね。
通知ロジックの実装
それでは、通知ロジックを実装しましょう。
controlTextDidChange:
に渡されたNSNotification
の情報を元に、どのフィールドが変更されたのかを判定して、適切な通知関数を呼んでやれば良さそうです。Haskellでは、ポインタの番地を比較することが簡単にできるので、changed
を次の様にかきかえれば良さそうです:
changed :: Session -> NSNotification -> IO ()
= do
changed session notif <- notif # sender
sendF <- application session # rateField
rateF <- application session # dollarsField
dollF if sendF == rateF
then pushRate session =<< rateF # doubleValue
else pushDollars session =<< dollF # intValue
上で述べたロジックをそのまま書いているだけですね。 必要なNSNotification
およびsender
の定義は以下の通りです(AppDelegate.hs
の適切な位置に挿入しておいてください):
"NSNotification" (Just ''NSObject)
defineClass 'NSNotification
idMarshaller '
= "sender"
defineSelector newSelector { selector = (''NSNotification, "notif")
, reciever = Just [t| NSTextField |]
, returnType = [cexp| [notif object] |]
, definition }
他のhogeField
系やhogeValue
系の定義はだいたい他の関数から類推出来ると思うので割愛します。
最後に、objc_implemenatation
中のcontrolTextDidChange:
の実装を以下のように変更すれば、万事OKです:
- (void) controlTextDidChange:(typename NSNotification*) aNotification
{
(self.session, aNotification);
changed}
以上で完成です(完成したコード)。
あとはさっき作っておいたBuilder.hs
を使ってコンパイルすれば良いだけですね:
$ runhaskell Builer.hs clean && runhaskell Builder.hs
起動すれば、望む通りの挙動をするようになっています!やりました!
補遺:マーシャラの書き方、レコード型専用構文
今まで駆け足で Haskell による Cocoa アプリの開発法を紹介してきましたが、恒等変換以外のマーシャラを書かないで来ました。あるていどメモリ管理などに対する勘が働くなら、マーシャラは普通に書くことが出来ます。
たとえば、NSString
を要素に持つNSArray
型の値と、[String]
を相互変換したいと思ったら以下のようなコードを書けばよいです:
newtype NSString = NSString (ForeignPtr ())
newtype NSArray = NSArray (ForeignPtr ())
newtype NSMutableArray = NSMutableArray (ForeignPtr ())
objc_typecheck
nsArrToListOfStrings :: NSArray -> IO [String]
= do
nsArrToListOfStrings arr <- $(objc ['arr :> Class ''NSArray] $ ''Int <: [cexp| [arr count] |])
len 0..len -1] $ \ i ->
forM [$(objc ['arr :> Class ''NSArray, 'i :> ''Int] $ ''String <: [cexp| [arr objectAtIndex: i] |])
listOfStringsToNSArr :: [String] -> IO NSArray
= do
listOfStringsToNSArr strs <- $(objc [] $ Class ''NSMutableArray <: [cexp| [NSMutableArray array] |])
marr $ \str ->
forM_ strs $(objc ['marr :> Class ''NSMutableArray, 'str :> ''String] $
void [cexp| [marr addObject: str] |])$(objc ['marr :> Class ''NSMutableArray] $
Class ''NSArray <: [cexp| marr |])
objc_marshaller 'listOfStringsToNSArr 'nsArrToListOfStrings
大して難しいことはありませんね。マーシャル関数を定義する前に objc_typecheck
というマクロを呼んでいるところだけ目新しいですが、これは実際には何もしないマクロです。なぜこんなものがあるのかというと、強制的に「何もしない」マクロを展開させることで、そこまで定義された型の情報をそれ以下のマクロ定義で使えるようにするためです。上のobjc_typecheck
をコメントアウトすると、「NSArray
なんて型しらないよ!!」と叱られる筈です。
さて、このように定義すれば、Haskell側の [String]
型と、Obj-C側のNSArray
クラスは以後同一視され、自動的に変換されるようになります。ここで大事なのは、Haskellの型とObj-Cのクラスが一対一に対応するようにすることです。つまり、ここで定義した以外に
NSArray
と [Int]
のマーシャラを定義することは出来ない、ということです。
これを回避するには、NSArray
を NSArray e
として、要素の型を表すダミーの幽霊型を引数に持つようにして、NSArray Int
と[Int]
、NSArray String
と[String]
の間のマーシャラを別個に定義してやれば問題ないです。
また、Haskellのレコード型とObj-Cの値を一対一にマップするための特別なマクロも用意されています。これについては今回は面倒臭くなってきたので(!)詳しくは触れませんが、language-c-inline
レポジトリにあるサンプルをみてみるとだいたい感じが掴めるんじゃないかと思います。おわり。
おわりに
ここまで、簡単なアプリケーションの開発を通して、HaskellによるCocoaアプリ開発の手法を概観して来ました。 いささか簡単すぎるので、これで本当に良い感じの Cocoa アプリが開発出来るの?と疑問に思われる方もおられるかもしれません。
そうした疑問に対する答えとして、私が適当に開発中の Haskell による OSX 向け SKK 実装、λSKK を挙げておきます。あ、時と場合によってデバッグ用途でキー入力をログに出力していたりして、ちょっとしたキーロガーみたいになってしまう場合があるので、これ使う時はパスワードとか大事な文字列を入力しないようにしてくださいね。とまれ、ビルドして使ってみると、まあまだバグはありますが結構ふつうに使えるアプリになっているのがわかると思います。
ひとまず、language-c-inline
を使えば、一応実用的なCocoaアプリが作れることがわかったと思います。とはいえ、マーシャル関数をモジュールを跨いで共有出来ないとか、まだまだ改善点はあると思います。また、出来ればAppDelegate
とSession
も同一視できるといいなあと思うんですが、なんか上手くいきませんでした。
また、ここで構築した簡易オブジェクトシステムについて改善出来るところを考えてみると、以下のような感じになるかな、と思います:
id
型への対応- コンテナ型の要素に関する共変性・不変性・反変性などの指定
- HaskellのFRP ライブラリと Cocoa Bindings の連携
まあ、id
については常に全ての型のサブクラスになっているように定義すればいいでしょう。
型パラメータの共変性等の指定については、ちょっとマクロが煩雑にならないように色々と考えてやる必要性がありそうです。そもそも、これをちゃんとやろうと思ったら、幽霊型の部分も含めて継承関係に含めなければいけないような気がするので、ちょっと骨が折れそうですね。
最後のHaskellのFRP ライブラリと Cocoa Bindings の連携については、ぼくはどちらにも詳しくないのでまだよくわからないです。ただ、パラダイムとしては Cocoa Bindings と FRP の考え方は似ているように思うので、なんらかのグルーコードを一度書いてしまえばうまいこと連携できるんじゃないかなー、などと考えています。また、ぼくはまったく触ってみたことがないのですが、よりFRP の影響を顕著に受けた Reactive Cocoa というObj-Cフレームワークがあるらしいので、そちらの方も頃合いを見て調べられたらなと思ってます。
というわけでやたら長々と Haskell による Cocoa アプリ開発の実際を説明してきましたが、如何だったでしょうか?後半かなり失速した感がありますが、いちおうこれで開発に必要な知識は纏められたと思います。是非何かしらの Cocoa アプリを実装してみて、面白いものが出来たら(面白くなくても)おしえてください。
それでは、Happy Haskell-Cocoa Development and Have a nice year!