LoginSignup
2

More than 3 years have passed since last update.

posted at

updated at

Etaで表現されるデータ型としてのJavaクラスとその継承関係

 この記事はHaskell Advent Calendar 2017
クリスマス当日(25日目)の記事です!

皆さん、アドベントカレンダーお疲れ様でした! :tada: :tada: :tada:

:sunflower: 日曜日お日様Haskeller :sunflower:

はじめに

 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 ()

余談

 もしかしたら::の後に改行を挟むスタイルに違和感を覚えたかもしれません。
例えば以下のように書きたいとか思ったかもしれません。

おめでとうございます、僕と同類です。

(こんな風に書きたかった :point_down:

foreign import java unsafe "@new"
  newException :: String -> Java a Exception

しかし現状、これはhasktagsがnewExceptionを検知してくれなくなります。

もしくは一行で書ききってしまうこともできますが、一行が長くなることに注意。

(以下のどちらかの書式は、hasktagsに認識してもらえる :point_down:

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 aList 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
など見てみると面白いと思います

参考ページ

「チッ」なんて言ってごめんね、ArrayList

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
2