この記事は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-inline0.7系統)には無い機能を使っています。なので、以下の作業をする上ではGitHubから直接最新版を取ってくるのが一番やりやすいと思います。そのうち最新版がリリースされる筈ですが、作者の方が忙しいので、Hackage に上がるのはもう暫く待ったほうが良いようです。

開発チュートリアル

Hello, World!

色々と能書きを述べてきましたが、まあ取り敢えず Hello, World をやってみましょう。

{-# LANGUAGE QuasiQuotes, TemplateHaskell #-}
module Main where
import Language.C.Inline.ObjC
import Language.C.Quote.ObjC

objc_import ["<Foundation/Foundation.h>"]

nsLog :: String -> IO ()
nsLog msg = $(objc ['msg :> ''String] $
              void [cexp| NSLog(@"%@", msg) |])

objc_emit

main = do
  objc_initialise
  nsLog "Hello, from Haskell-Cocoa!"

一つずつ解説していきましょう。まず四行目の 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 との間にアウトレット、アクションを設定してください:

ウインドウの初期配置と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

objc_import ["<Cocoa/Cocoa.h>"]

では、メインロジックである変換関数convertを実装してみましょう。 通貨換算器という大仰な名前でも、結局やることは

  1. 換算前の価格(dollarsField)と為替レート(rateField)の値を取得し、
  2. 両者を掛け算して、
  3. その結果を表示(resultField)する。

ということだけで、実質掛け算するだけです。Cocoa のリファレンスマニュアルなどと首っぴきになれば、テキストフィールドの値の取得・設定は-(int) intValue-(double) doubleValue-(void) setIntValue:(double) valなどを使えばよさそうなので、メインロジックであるconvertの実装は次のように書けば良さそうです:

convert :: AppDelegate -> IO ()
convert app = do
  input <- intValue    =<< dollarsField app
  rate  <- doubleValue =<< rateField app
  setIntValue (floor $ fromIntegral input * rate) =<< resultField app

このあとすべき事が幾つかあります:

  1. AppDelegateNSTextFieldなどに対応する型をHaskell側で定義する
  2. intValue, doubleValue, setIntValue の実装
  3. Obj-C 側のAppDelegateクラスのconvert: アクションと上で実装したconvert関数との紐付けとmain関数の実装
  4. アプリケーションバンドルの作成

それぞれ順に見ていきましょう。

Obj-C のオブジェクトを Haskell で扱うには?

language-c-inlineでは、基本型(int, BOOL, charなど)以外のObj-Cの値は、ForeignPtrnewtypeで包んだ型で表現されます。たとえば、AppDelegate に対応するHaskellの型は、

newtype AppDelegate = AppDelegate (ForeignPtr AppDelegate)
                      deriving (Typeable)

のように宣言されます。型コンストラクタの名前が、そのままObj-Cでの型名として扱われます。現時点におけるlanguage-c-inlineライブラリの内部実装では動的キャストを使っているため、Typeable のインスタンスを導出させておく必要があります。また、これも内部実装の制約から、newtype宣言はレコード型ではなく、このようなコンストラクタのみの形で宣言される必要があることに注意してください。

実際には、AppDelegateの値をやりとりするには、次のような行を追加しておく必要があります:

marshalAppDel :: AppDelegate -> IO AppDelegate
marshalAppDel = return

objc_marshaller 'marshalAppDel 'marshalAppDel

字面からなんとなくわかるかもしれませんが、ここではAppDelegate型の値をHaskellとObj-Cの間でやりとりする際のマーシャリング関数を定義しています。objc_marshallerマクロは、それぞれA -> IO BB -> IO Aという形の型を持つ関数の名前をとって、A, B型の値を相互変換するための情報を保存します。このマーシャリングの情報はモジュールごとに保存されるので、モジュールを変えるたびにobjc_marshallerを呼び出して関数を登録してやる必要があることに注意してください。

今回の場合は、単にポインタに包まれた値をやりとりするだけなので、A = B = AppDelegateとして何もせずにreturnするだけの関数を登録しています。

同様にして、NSTextFieldを表す型も定義できます:

newtype NSTextField = NSTextField (ForeignPtr NSTextField)
                      deriving (Typeable)

marshalNSTextField :: NSTextField -> IO NSTextField
marshalNSTextField = return

objc_marshaller 'marshalNSTextField 'marshalNSTextField
定義順に御用心

ここで、一つ注意があります。こうしたラッパ型や、次の節で定義するラッパ関数は、それを呼び出す関数の定義より前にもってくる必要があります。Haskellでは通常関数の定義順に注意する必要はありませんが、language-c-inlineではTemplateHaskellのマクロをふんだんに使っているので、マクロが走る時点において型・関数の情報が必要となるため、このような制約があります。

ラッパ関数の定義

intValuesetIntValue、あるいはdollarsFieldアウトレットなどを取得するためのHaskell関数は、Hello, World!の例でNSLogのラッパ関数を定義した時と同様にして、objc マクロを使って定義してやれば十分です。ためしにintValue, rateField, setIntValueの定義を見てみましょう:

intValue :: NSTextField -> IO Int
intValue txt = $(objc ['txt :> ''NSTextField] $
                 ''Int <: [cexp| [txt intValue] |])

rateField :: AppDelegate -> IO NSTextField
rateField app = $(objc ['app :> ''AppDelegate] $
                     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のオブジェクトである、という意味のヒントのようですね。

こうしたことを踏まえて上の定義を見てみれば、あとはそんなに驚くようなことはやっていないと思います。 これらに習って、doubleValuedollarField, 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のコードをパーズするのにあらかじめ型の情報が必要となるため用意されています。intdouble, 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 ()
nsApplicationMain = $(objc [] $ void [cexp| NSApplicationMain(0, NULL) |])

objc_emit

main :: IO ()
main = do
  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
cast (Object ptr) = Object ptr

ダウンキャストも同様に実装出来ますが、まあ安全ではないので 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 ()
  send' ctrl (SetObjectValue num) = $(objc ['ctrl :> Class ''NSControl, 'obj :> Class ''NSObject] $
                                      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
obj # f = liftIO $ send' obj f

infixl 4 #.
(#.) :: (a :> b, Selector a msg, Returns msg ~ IO c, MonadIO m) => IO (Object b) -> Message msg -> m c
recv #. sel = liftIO $ recv >>= flip send' sel

(#)が中置演算子版で、のものは、メソッドチェーンにして呼び出していくときに、便利なように定義されたものです。

継承ツリーをちゃんと機能させるために

ところで、今までに定義してきた継承関係の推論規則には、反射律(A は A 自身のサブクラス)しか入っていません。このままだと、サブクラスのサブクラスが元のスーパークラスのサブになりません3。では、推移律を条件に入れてはどうでしょうか?

instance (a :> b, b :> c) => a :> c

残念ながら、これはうまくいきません。a :> cのインスタンスを得るのに、間に出て来る b をどう扱えば良いかわからないからです。 これには、たとえば、単純な継承関係をやめて、例えば継承関係を一段ずつ型レベルリストにして型パラメータで持たせるといった方法が考えられます。しかし、なんだかそれは余りにもオーバーキルです。他にも型機能を駆使して実現する方法もあるかもしれませんが、ここでは違う方針を取ります。

どうするのかというと、直接 (:>) のインスタンスを宣言するのではなく、それまでの型情報を使って、スーパークラスのスーパークラス、更にそのスーパークラス……と辿っていって、推移律を満たすのに必要なインスタンスを宣言するような Template Haskell マクロを実装してしまうことにします。

という訳でそのように実装されているのが、defineClassおよびdefinePhantomClassマクロです(それぞれMessaging.Macroで定義されています)。たとえば、NSObject, NSString, NSMutableStringなどの継承関係を定義するには、次のようにすれば、万事面倒を見てくれます:

defineClass "NSObject" Nothing
defineClass "NSString" $ Just ''NSObject
defineClass "NSMutableString" $ Just ''NSString

つまり、NSObject :> NSString, NSString :> NSMutableString, NSObject :> NSMutableStringといった、必要な(:>)のインスタンスがこれにより全て生成され、type NSObject = Object "NSObject"などの型シノニムも定義されます。

definePhantomClass は、更に幽霊型を引数にもつように定義してくれます。たとえば、

definePhantomClass 1 "NSArray" $ Just ''NSObject

のようにすると、type NSArray a = Object "NSArray" といったような、型シノニムが宣言されるのです。ただ、これは今のところ共変性や反変性とかを全く考慮していないので、あんまり嬉しくないかもしれません。

これで必要な継承関係は面倒を見てくれるようになりましたが、このままだと折角継承関係を定義したのに、手動でアップキャストしたりしながらメッセージを送らないといけません。そこで、まずは受け手を自動でアップキャストしてくれるような send を実装しましょう:

send :: (a :> b, Selector a msg) => Object b -> Message msg -> Returns msg
send = send' . cast

単にcastを前に合成してやればいいだけですね。問題は、メッセージの引数のアップキャストですが、各メッセージに対してたとえば以下のようにしてObject型の引数を取る引数にcastを被せた便利関数を定義すればよさそうです:

setObjectValue :: ("NSObject" :> a) => a -> Message "setObjectValue"
setObjectValue obj = SetObjectValue (cast obj)

こうしたものを一々定義するのは面倒なので、これもマクロで解決してしまいましょう。ということで、造られたのが defineSelector関数です。これを使うと、たとえばsetObjectValue の定義は次のようになります:

defineSelector newSelector { selector = "setObjectValue"
                           , reciever = (''NSControl, "ctrl")
                           , arguments = ["obj" :>>: Class ''NSObject]
                           , definition = [cexp| [ctrl setObject: obj] |]
                           }

:>>:は、language-c-inline:>:を模倣して定義した型です。模倣したのは、language-c-inlineHint回りのインスタンス等を後付けで増やせなかったからです。多分。また、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.cabalBuilder.hsはそれぞれ左のリンクから取得しておいてください。また、Messaging関連はobjc-messagingのレポジトリからダウンロードして、srcに放り込んでおいてください。

Builder.hsを動かすには、shakeビルドシステムが必要なので、cabal install shakeとしておいてください。この記事はShakeの入門記事ではないので、Builder.hsの内容についてはとくに立ち入らないことにします。

はじめの一歩

ツールの整備を強引に終わらせたので、まずは次のように.xibファイルを変更しましょう:

ボタンとアクションを削除し、代わりにAppDelegateを各フィールドのデリゲートに指定する

今回は main 関数の部分と、AppDelegateの実装を分離することにします。上のディレクトリ構成図通りsrcディレクトリにMain.hsAppDelegate.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

objc_import ["<Cocoa/Cocoa.h>"]

nsApplicationMain :: IO ()
nsApplicationMain = $(objc [] $ void [cexp| NSApplicationMain(0, NULL) |])

objc_emit

main :: IO ()
main = do
  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
newSession app = sync $ do
  (dolBh, dolL) <- newBehaviour 0
  (ratBh, ratL) <- newBehaviour 0
  _ <- listen (value $ (*) <$> dolBh <*> ratBh) $ \val ->
    app # resultField #. setIntValue (floor val)
  return $ Session (sync . dolL . fromIntegral) (sync . ratL) app

まず、内部状態を表すSession型はドル欄とレート欄の変化を通知する関数pushDollarsおよびpushRate、そして現在走っているAppDelegateの三つ組として定義しています。フィールド内容の変更が通知されたら、これらの関数を通してFRPエンジン側に変更を伝達する訳です。

そうした通知関数を作成しているのが newSession 関数です。FRPにおいてはBehaviorというのは連続的に変化する値を表します。newSessionの二、三行目で、それぞれドル値とレート値を表現するBehaivourと変更通知関数を生成しています。

それらの変更があった時に実際に結果を更新する設定をしているのが、listenを呼んでいる続く二行です。(#)(#.)は先程定義したメッセージ送信用の中置演算子です。ここで使われているメッセージやクラスは次のように定義されています:

defineClass "NSObject" Nothing

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
 |]

defineClass "AppDelegate" (Just ''NSObject)
idMarshaller ''AppDelegate

defineClass "NSTextField" (Just ''NSObject)
idMarshaller ''NSTextField

defineSelector newSelector { selector = "setIntValue"
                           , reciever = (''NSTextField, "field")
                           , arguments = ["num" :>>: ''Int]
                           , definition = [cexp| [field setIntValue: num] |]
                           }
defineSelector newSelector { selector = "resultField"
                           , reciever = (''AppDelegate, "app")
                           , definition = [cexp| [app resultField] |]
                           , returnType = Just [t| NSTextField |]
                           }

ここで 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 ()
changed session notif = do
  sendF <- notif # sender
  rateF <- application session # rateField
  dollF <- application session # dollarsField
  if sendF == rateF
    then pushRate    session =<< rateF # doubleValue
    else pushDollars session =<< dollF # intValue

上で述べたロジックをそのまま書いているだけですね。 必要なNSNotificationおよびsenderの定義は以下の通りです(AppDelegate.hsの適切な位置に挿入しておいてください):

defineClass "NSNotification" (Just ''NSObject)
idMarshaller ''NSNotification

defineSelector newSelector { selector = "sender"
                           , reciever = (''NSNotification, "notif")
                           , returnType = Just [t| NSTextField |]
                           , definition = [cexp| [notif object] |]
                           }

他のhogeField系やhogeValue系の定義はだいたい他の関数から類推出来ると思うので割愛します。

最後に、objc_implemenatation中のcontrolTextDidChange:の実装を以下のように変更すれば、万事OKです:

- (void) controlTextDidChange:(typename NSNotification*) aNotification
{
  changed(self.session, aNotification);
}

以上で完成です(完成したコード)。

あとはさっき作っておいた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]
nsArrToListOfStrings arr = do
  len <- $(objc ['arr :> Class ''NSArray] $ ''Int <: [cexp| [arr count] |])
  forM [0..len -1] $ \ i ->
    $(objc ['arr :> Class ''NSArray, 'i :> ''Int] $ ''String <: [cexp| [arr objectAtIndex: i] |])

listOfStringsToNSArr :: [String] -> IO NSArray
listOfStringsToNSArr strs = do
  marr <- $(objc [] $ Class ''NSMutableArray <: [cexp| [NSMutableArray array] |])
  forM_ strs $ \str ->
    $(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] のマーシャラを定義することは出来ない、ということです。

これを回避するには、NSArrayNSArray e として、要素の型を表すダミーの幽霊型を引数に持つようにして、NSArray Int[Int]NSArray String[String]の間のマーシャラを別個に定義してやれば問題ないです。

また、Haskellのレコード型とObj-Cの値を一対一にマップするための特別なマクロも用意されています。これについては今回は面倒臭くなってきたので(!)詳しくは触れませんが、language-c-inlineレポジトリにあるサンプルをみてみるとだいたい感じが掴めるんじゃないかと思います。おわり。

おわりに

ここまで、簡単なアプリケーションの開発を通して、HaskellによるCocoaアプリ開発の手法を概観して来ました。 いささか簡単すぎるので、これで本当に良い感じの Cocoa アプリが開発出来るの?と疑問に思われる方もおられるかもしれません。

そうした疑問に対する答えとして、私が適当に開発中の Haskell による OSX 向け SKK 実装、λSKK を挙げておきます。あ、時と場合によってデバッグ用途でキー入力をログに出力していたりして、ちょっとしたキーロガーみたいになってしまう場合があるので、これ使う時はパスワードとか大事な文字列を入力しないようにしてくださいね。とまれ、ビルドして使ってみると、まあまだバグはありますが結構ふつうに使えるアプリになっているのがわかると思います。

ひとまず、language-c-inlineを使えば、一応実用的なCocoaアプリが作れることがわかったと思います。とはいえ、マーシャル関数をモジュールを跨いで共有出来ないとか、まだまだ改善点はあると思います。また、出来ればAppDelegateSessionも同一視できるといいなあと思うんですが、なんか上手くいきませんでした。

また、ここで構築した簡易オブジェクトシステムについて改善出来るところを考えてみると、以下のような感じになるかな、と思います:

  • 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!


  1. 発表日は遅延評価されました(常套句)。↩︎

  2. 理論上は ghc-ios と FFI を使えば iOS アプリを開発することも可能ですが、ghc-ios は執筆時点で Template Haskell に対応していないため、以下で説明する方法はまだ使えません。↩︎

  3. お前は何をいってるんだ。↩︎

  4. Cocoa Bindings も原始的な FRP みたいなものなので、Haskell側のFRPライブラリと上手いこと連携が取れるとよいなあ、と思っているのですが、今のところあんまり良い方法が思い付いてません。↩︎


Comments