この記事はHaskell Advent Calendar 2017の
クリスマス当日(25日目)の記事です!
皆さん、アドベントカレンダーお疲れ様でした!
日曜日お日様Haskeller
はじめに
EtaはJVM上で扱える、GHC7.10.3互換のHaskellコンパイラです。
この記事の内容はEtaのバージョン 0.0.9b6
に準拠します。
本記事はEtaの入門のためというよりも、雰囲気の触りになればいいかなというレベルで書いていきます。
とはいえ、参考にされることを考慮して書いたつもりです。
完全な入門に際しては、公式ドキュメントがかなりわかりやすいです。
Javaのクラスを定義する
各々について、以下のように定義できます。
データ型とコンストラクタ
data Exception = Exception @java.lang.Exception
deriving (Class)
Exceptionを使うにはJava上と同じように、コンストラクタが必要です。
以下のように定義できます。
-- Exception(String message)コンストラクタに対応
foreign import java unsafe "@new" newException ::
String -> Java a Exception
-- Exception()コンストラクタに対応
foreign import java unsafe "@new" newException' ::
Java a Exception
ジェネリクスを使ったデータ型は高階データ型として定義されます。
data ArrayList a = ArrayList (@java.util.ArrayList a)
deriving (Class)
foreign import java unsafe "@new" newArrayList ::
Java a (ArrayList c)
完全なコードはこれです。
import Java
data Exception = Exception @java.lang.Exception
deriving (Class)
foreign import java unsafe "@new" newException ::
String -> Java a Exception
foreign import java unsafe "@new" newException' ::
Java a Exception
data ArrayList a = ArrayList (@java.util.ArrayList a)
deriving (Class)
foreign import java unsafe "@new" newArrayList ::
Java a (ArrayList c)
-- java :: (forall c. Java c a) -> IO a
main :: IO ()
main = java $ do
exception <- newException "pero"
arrayList <- newArrayList
return ()
余談
もしかしたら::
の後に改行を挟むスタイルに違和感を覚えたかもしれません。
例えば以下のように書きたいとか思ったかもしれません。
おめでとうございます、僕と同類です。
(こんな風に書きたかった )
foreign import java unsafe "@new"
newException :: String -> Java a Exception
しかし現状、これはhasktagsがnewException
を検知してくれなくなります。
もしくは一行で書ききってしまうこともできますが、一行が長くなることに注意。
(以下のどちらかの書式は、hasktagsに認識してもらえる )
foreign import java unsafe "@new" newException ::
String -> Java a Exception
foreign import java unsafe "@new" newException :: String -> Java a Exception
メソッドとフィールド
foreign import java unsafe "size" size ::
Java (ArrayList a) Int
定義したメソッドはこのように実行できます。
-- (<.>) :: Class c => c -> Java c a -> Java b a
do
arrayList <- newArrayList
n <- arrayList <.> size
-- arrayList <.> size :: Java a Int
-- n :: Int
...
フィールドはこのように……
チッ、ArrayList
にpublicフィールドがねえな。
data Point = Point @java.awt.Point
deriving (Class)
foreign import java unsafe "@new" newPoint ::
Int -> Int -> Java a Point
foreign import java safe "@field x" pointX ::
Point -> Int
フィールドへのアクセスは副作用がないので、safe
を指定して、純粋関数として宣言してもよいでしょう。
(そう、Etaは{a-class} -> {field-type}
という型宣言も許容できるのです)
safe
はノンブロッキングで実行されるので、パフォーマンスの向上が期待できます。
しかしその安全性はプログラマの責任なので、基本はunsafe
、
外部から値が設定され得ないフィールドの取得についてはsafe
、
という感じでいいのではないでしょうか。
メソッドチェーンは>-
によって行えます。
-- (>-) :: Class b => Java a b -> Java b c -> Java a c
do
n <- newArrayList >- size
...
staticメソッドとstaticフィールドに関しては、""
の中のクラスパスをフルパスで記述する必要があります。
foreign import java unsafe "@static @field java.lang.Math.E" logarithmBaseE ::
Java a Double
完全なソースは以下です。
import Java
data Exception = Exception @java.lang.Exception
deriving (Class)
foreign import java unsafe "@new" newException ::
String -> Java a Exception
foreign import java unsafe "@new" newException' ::
Java a Exception
data Throwable = Throwable @java.lang.Throwable
deriving (Class)
data ArrayList a = ArrayList (@java.util.ArrayList a)
deriving (Class)
foreign import java unsafe "@new" newArrayList ::
Java a (ArrayList c)
foreign import java unsafe "size" size ::
Java (ArrayList a) Int
data Point = Point @java.awt.Point
deriving (Class)
foreign import java unsafe "@new" newPoint ::
Int -> Int -> Java a Point
foreign import java safe "@field x" pointX ::
Point -> Int
foreign import java unsafe "@static @field java.lang.Math.E" logarithmBaseE ::
Java a Double
main :: IO ()
main = java $ do
exception <- newException "pero"
arrayList <- newArrayList
point <- newPoint 1 2
print' $ pointX point
print' =<< logarithmBaseE
print' =<< arrayList <.> size
print' =<< newArrayList >- size
where
-- Java aはIOと同等なので、以下のような関数でprintできる
print' :: Show a => a -> Java c ()
print' = io . print
余談
このFFIで指定された""
の中身は動的に解決されるので、この指定を謝ると、いとも容易くランタイムエラーを出します。
これはsize
メソッドにtypoで「ぼ」というサフィックスを付けてしまった例です。
import Java
data ArrayList a = ArrayList (@java.util.ArrayList a)
deriving (Class)
foreign import java unsafe "@new" newArrayList ::
Java a (ArrayList c)
-- ArrayListに「sizeぼ」というインスタンスメソッドはない
foreign import java unsafe "sizeぼ" size ::
Java (ArrayList a) Int
main :: IO ()
main = java $ do
arrayList <- newArrayList
n <- arrayList <.> size
io $ print n
-- [1 of 1] Compiling Main ( /home/aiya000/.tmp/Test.hs, /home/aiya000/.tmp/Test.jar )
-- Linking /home/aiya000/.tmp/RunTest.jar ...
-- Exception in thread "main" java.lang.NoSuchMethodError: java.util.ArrayList.sizeぼ()I
-- at main.Main$$Lr3B3a1.applyO(Test.hs)
-- ...
-- at eta.main.main(Unknown Source)
そしてnewException (10 :: Double)
というもののはFFIによって、
JVM上でのnew Exception(10)
という値レベルの動作として成されるので、
これもランタイムエラーになります。
import Java
data Exception = Exception @java.lang.Exception
deriving (Class)
-- Exception(double x)というコンストラクタは存在しない
foreign import java unsafe "@new" newException ::
Double -> Java a Exception
main :: IO ()
main = java $ do
exception <- newException 10
return ()
-- [1 of 1] Compiling Main ( /home/aiya000/.tmp/Test.hs, /home/aiya000/.tmp/Test.jar )
-- Linking /home/aiya000/.tmp/RunTest.jar ...
-- Exception in thread "main" java.lang.NoSuchMethodError: java.lang.Exception.<init>(D)V
-- at main.Main$sat_s2L9.applyO(Test.hs)
-- ...
-- at eta.main.main(Unknown Source)
GHCにおけるFFIと同様、Haskell側の型宣言と、import先の実定義には乖離が生じるので、
Haskell側の型宣言についての注意が必要です。
(乖離: ここでは、実装と型定義が相違していても、コンパイルが通ること)
継承関係
ところで例えばException
クラスはThrowable
クラスを継承しています。
Etaはそのような継承関係を型レベルで解決します。
(型レベルのvtableを定義させます)
具体的にはInherits
type familyで、そのドメインをコドメインのクラス達のサブクラスとして登録します。
type instance Inherits A = '[B]
は
インスタンス定義instance (Class a, Class b, Extends' a b ~ Yes) => Extends a b
により、Extends A B
インスタンスになります。
data Throwable = Throwable @java.lang.Throwable
deriving (Class)
data Exception = Exception @java.lang.Exception
deriving (Class)
type instance Inherits Exception = '[Throwable]
サブクラスはsuperCast :: Extends a b => a -> b
によって、スーパークラスに型変換できます。
main :: IO ()
main = java $ do
exception <- newException "lala"
return (superCast exception :: Throwable)
return ()
唯一Object
クラスに関しては例外です。
任意のClass
インスタンス(Javaクラスデータ型)は、
Inherits
に関わらずObject
クラスのサブクラスです。
data Exception = Exception @java.lang.Exception
deriving (Class)
foreign import java unsafe "@new" newException ::
String -> Java a Exception
data ArrayList a = ArrayList (@java.util.ArrayList a)
deriving (Class)
foreign import java unsafe "@new" newArrayList ::
Java a (ArrayList c)
main :: IO ()
main = java $ do
exception <- newException "lala"
arrayList <- newArrayList
return (superCast exception :: Object)
return (superCast arrayList :: Object)
return ()
ArrayList a
はList a
インターフェースのサブクラスですが、インターフェースについても同様です。
(インターフェースもクラスと同様にはデータ型であって、つまりEta上ではどちらもあまり区別されていないと思います。
強いて言えばただ、コンストラクタになる関数がありません)
data List a = List (@java.util.List a)
deriving (Class)
data RandomAccess = RandomAccess @java.util.RandomAccess
deriving (Class)
data ArrayList a = ArrayList (@java.util.ArrayList a)
deriving (Class)
type instance Inherits (ArrayList a) = '[List a, RandomAccess]
foreign import java unsafe "@new" newArrayList ::
Java a (ArrayList c)
foreign import java unsafe "size" size ::
Java (ArrayList a) Int
main :: IO ()
main = java $ do
arrayList <- newArrayList
return (superCast arrayList :: List Int)
return (superCast arrayList :: RandomAccess)
return ()
親の親は親です。
data Serializable = Serializable @java.lang.Serializable
deriving (Class)
data Throwable = Throwable @java.lang.Throwable
deriving (Class)
type instance Inherits Throwable = '[Serializable]
data Exception = Exception @java.lang.Exception
deriving (Class)
type instance Inherits Exception = '[Throwable]
foreign import java unsafe "@new" newException ::
String -> Java a Exception
main :: IO ()
main = java $ do
exception <- newException "lala"
return (superCast exception :: Throwable)
return (superCast exception :: Serializable)
return ()
以下が完全なコードです。
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE TypeFamilies #-}
import Java hiding (List)
data Serializable = Serializable @java.lang.Serializable
deriving (Class)
data Throwable = Throwable @java.lang.Throwable
deriving (Class)
type instance Inherits Throwable = '[Serializable]
data Exception = Exception @java.lang.Exception
deriving (Class)
type instance Inherits Exception = '[Throwable]
foreign import java unsafe "@new" newException ::
String -> Java a Exception
data List a = List (@java.util.List a)
deriving (Class)
data RandomAccess = RandomAccess @java.util.RandomAccess
deriving (Class)
data ArrayList a = ArrayList (@java.util.ArrayList a)
deriving (Class)
type instance Inherits (ArrayList a) = '[List a, RandomAccess]
foreign import java unsafe "@new" newArrayList ::
Java a (ArrayList c)
foreign import java unsafe "size" size ::
Java (ArrayList a) Int
main :: IO ()
main = java $ do
arrayList <- newArrayList
return (superCast arrayList :: List Int)
return (superCast arrayList :: RandomAccess)
exception <- newException "lala"
return (superCast exception :: Throwable)
return (superCast exception :: Serializable)
return ()
余談
unsafeCast :: Extends a b :: b -> a
という関数があります。
名前の通り、実行時エラーを引き起こす可能性があります。
既存の ライブラリ/フレームワーク をEtaで使おうとするとこれに頼ることになってしまうかと思いますが、
避けられる場合は避けた方がいいです。
余談
Extends
の別名に<:
という型演算子があったりします。
余談
任意の インターフェースがObject
の子になってる 🤔
main :: IO ()
main = java $ do
exception <- newException "lala"
let x = superCast exception :: Serializable
return (superCast x :: Object)
return ()
おわりに
Etaは継承関係の解決に、型レベルプログラミングを実用しています。
もし貴方が「型レベルプログラミングって何の訳に立つの?」って聞かれた時は
「HaskellがJVMで使えるようになる」って答えてあげましょう。
(
定義instance (Class a, Class b, Extends' a b ~ Yes) => Extends a b where
など見てみると面白いと思います
)
参考ページ
- Eta Tutorials - Eta documentation
- GitHub - typelead/eta: The Eta Programming Language, a dialect of Haskell on the JVM
- Exception (Java Platform SE 8)
- ArrayList (Java Platform SE 7)
- GitHub - MarcWeber/hasktags: Produces ctags "tags" and etags "TAGS" files for Haskell programs
「チッ」なんて言ってごめんね、ArrayList
。