読者です 読者をやめる 読者になる 読者になる

IT練習ノート

IT関連で調べたこと(実際は嵌ったこと)を書いています。

Servantでの例外処理

ファイルの読み込み(基本形で、ここではまだ例外処理を入れていない)

Reading a file in Servant

ファイル読み込みに失敗した時の対処 (catch)

*> :t catch
catch :: Exception e => IO a -> (e -> IO a) -> IO a

catchは、catchした時の対処はできるが、戻りの型はIO aなので、aで正常と異常を区別しなくてはいけない。下の例ではIO Stringなので、Stringで正常と異常を区別することになるが、異常の状態をなにか特別な文字列表現にしなくてはいけない。これは、実質的に、例外の情報を、呼び出し側に教えることができない。

Exception handing by catch in Servant

ファイル読み込みに失敗した時の対処 (try)

tryIO (Either a b)なので、例外の情報をLeftで呼び出し元に教えることができる。

Exception handling by try in Servant

パスより読み込むファイルを指定する

Capturing in Servant

明示的に型を指定するVisible Type Application

GHC 8.0からVisible Type Applicationが導入されました。

TypeApplication – GHC

今までは::で型を明示的に指定していました。

Prelude> 
Prelude> let x = return 1
Prelude> :t x
x :: (Num a, Monad m) => m a
Prelude> let x = return 1 :: Maybe Int
Prelude> :t x
x :: Maybe Int
Prelude> let x = return 1 :: Maybe Float
Prelude> :t x
x :: Maybe Float
Prelude> 

TypeApplicationsを使うと@Typeで型を指定することができるようになります。

Prelude> 
Prelude> :set -XTypeApplications
Prelude> let x = return @Maybe 1
Prelude> :t x
x :: Num a => Maybe a
Prelude> 

使い方としては、ポリモーフィックな関数があったとして、それに対して型を限定した使い方をさせたい時に使うのかもしれません。

Prelude> :t map
map :: (a -> b) -> [a] -> [b]
Prelude> :t map @Int @Char
map @Int @Char :: (Int -> Char) -> [Int] -> [Char]

IntとIntegerの違い

IntIntegerとの違い

インスタンスの確認

Prelude > :i Int
data Int = GHC.Types.I# GHC.Prim.Int#    -- Defined in ‘GHC.Types’
instance Bounded Int      -- Defined in ‘GHC.Enum’
instance Enum Int         -- Defined in ‘GHC.Enum’
instance Eq Int           -- Defined in ‘GHC.Classes’
instance Integral Int     -- Defined in ‘GHC.Real’
instance Num Int          -- Defined in ‘GHC.Num’
instance Ord Int          -- Defined in ‘GHC.Classes’
instance Read Int         -- Defined in ‘GHC.Read’
instance Real Int         -- Defined in ‘GHC.Real’
instance Show Int         -- Defined in ‘GHC.Show’
Prelude > :i Integer
data Integer
  = integer-gmp-1.0.0.1:GHC.Integer.Type.S# !GHC.Prim.Int#
  | integer-gmp-1.0.0.1:GHC.Integer.Type.Jp# {-# UNPACK #-}integer-gmp-1.0.0.1:GHC.Integer.Type.BigNat
  | integer-gmp-1.0.0.1:GHC.Integer.Type.Jn# {-# UNPACK #-}integer-gmp-1.0.0.1:GHC.Integer.Type.BigNat
                      -- Defined in ‘integer-gmp-1.0.0.1:GHC.Integer.Type’
instance Enum Integer     -- Defined in ‘GHC.Enum’
instance Eq Integer       -- Defined in ‘integer-gmp-1.0.0.1:GHC.Integer.Type’
instance Integral Integer -- Defined in ‘GHC.Real’
instance Num Integer      -- Defined in ‘GHC.Num’
instance Ord Integer      -- Defined in ‘integer-gmp-1.0.0.1:GHC.Integer.Type’
instance Read Integer     -- Defined in ‘GHC.Read’
instance Real Integer     -- Defined in ‘GHC.Real’
instance Show Integer     -- Defined in ‘GHC.Show’

BoundedクラスのインスタンスになっているのがIntで、一方、IntegerはBoundedクラスのインスタンスではない。

Boundedを確認してみると。Integerには最大値(maxBound)がないことがわかる。

Prelude > maxBound :: Int
9223372036854775807

Prelude > maxBound :: Integer

<interactive>:11:1: error:
    • No instance for (Bounded Integer)
        arising from a use of ‘maxBound’
    • In the expression: maxBound :: Integer
      In an equation for ‘it’: it = maxBound :: Integer

Intの最大値(9223372036854775807)の型を確認する。

Prelude > 9223372036854775807 :: Integer
9223372036854775807

Prelude > 9223372036854775807 :: Int
9223372036854775807

Intの最大値から+1の(9223372036854775807+1)の型を確認する。

Prelude > 9223372036854775808 :: Integer
9223372036854775808

Prelude > 9223372036854775808 :: Int

<interactive>:15:1: warning: [-Woverflowed-literals]
    Literal 9223372036854775808 is out of the Int range -9223372036854775808..9223372036854775807
    If you are trying to write a large negative literal, use NegativeLiterals
-9223372036854775808

Haskellで複素数

複素数のためのData.Complexパッケージがあります。

Data.Complex

複素数:+コンストラクタを使います。

> :i :+
data Complex a = !a :+ !a  -- Defined in ‘Data.Complex’
infix 6 :+

1+2iは次のよう:+を使って次のようになります。

> 1 :+ 2
1 :+ 2
> :t (1 :+ 2)
(1 :+ 2) :: Num a => Complex a

共役はconjugateを使います。

> Data.Complex.conjugate (1 :+ 2)
1 :+ (-2)

通常の加減乗除の演算子が使えます。

> let c1 = 1 :+ 2
> let c2 = 1 :+ (-2)
> c1 + c2
2.0 :+ 0.0
> c1 * c2
5.0 :+ 0.0
> c1 - c2
0.0 :+ 4.0
> c1 / c2
(-0.6) :+ 0.8

極形式も使えます。mkPolar 絶対値 偏角 で指定します。

> mkPolar 1 pi
(-1.0) :+ 1.2246467991473532e-16

複素数から絶対値 偏角 を取り出すことができます。

> polar (1 :+ 1)
(1.4142135623730951,0.7853981633974483)

> polar ((-1) :+ 0)
(1.0,3.141592653589793)

絶対値

> abs (1 :+ 1)
1.4142135623730951 :+ 0.0

Servantのexampleのcabalファイルを作成する

下記にあるexampleのgreet.hs用のcabalファイルを作ります。

github.com

cabal initにて対話形式で必要な項目を入力していきます。

 foo$ cabal init
Package name? [default: servant02] greet
Package version? [default: 0.1.0.0] 
Please choose a license:
   1) GPL-2
   2) GPL-3
   3) LGPL-2.1
   4) LGPL-3
   5) AGPL-3
   6) BSD2
 * 7) BSD3
   8) MIT
   9) ISC
  10) MPL-2.0
  11) Apache-2.0
  12) PublicDomain
  13) AllRightsReserved
  14) Other (specify)
Your choice? [default: BSD3] 8
Author name? [default: bar] foo
Maintainer email? [default: aaa@gmail.com] foo@gmail.com
Project homepage URL? 
Project synopsis? greet
Project category:
 * 1) (none)
   2) Codec
   3) Concurrency
   4) Control
   5) Data
   6) Database
   7) Development
   8) Distribution
   9) Game
  10) Graphics
  11) Language
  12) Math
  13) Network
  14) Sound
  15) System
  16) Testing
  17) Text
  18) Web
  19) Other (specify)
Your choice? [default: (none)] 18
What does the package build:
   1) Library
   2) Executable
Your choice? 2
What is the main module of the executable:
 * 1) Main.hs
   2) Other (specify)
Your choice? [default: Main.hs] 2
Please specify? greet.hs
Source directory:
 * 1) (none)
   2) src
   3) Other (specify)
Your choice? [default: (none)] 
What base language is the package written in:
 * 1) Haskell2010
   2) Haskell98
   3) Other (specify)
Your choice? [default: Haskell2010] 
Add informative comments to each field in the cabal file (y/n)? [default: n] y

Guessing dependencies...

Generating LICENSE...
Warning: LICENSE already exists, backing up old version in LICENSE.save0
Generating Setup.hs...
Warning: Setup.hs already exists, backing up old version in Setup.hs.save0
Generating ChangeLog.md...
Warning: ChangeLog.md already exists, backing up old version in ChangeLog.md.save0
Generating greet.cabal...

You may want to edit the .cabal file and add a Description field.

そのまま実行しようとすると、依存関係のライブラリの指定がなくてエラーになります。Guessing dependencies...とありますが、完全には推測してくれないようです。

 foo$ cabal run greet.cabal 
./greet.cabal has been changed. Re-configuring with most recently used
options. If this fails, please run configure manually.
Resolving dependencies...
Configuring greet-0.1.0.0...
Preprocessing executable 'greet' for greet-0.1.0.0...
[1 of 1] Compiling Main             ( greet.hs, dist/build/greet/greet-tmp/Main.o ) [Servant changed]

greet.hs:8:1: error:
    Failed to load interface for ‘Data.Aeson’
    It is a member of the hidden package ‘aeson-1.0.2.1’.
    Perhaps you need to add ‘aeson’ to the build-depends in your .cabal file.
    Use -v to see a list of the files searched for.

greet.hs:11:1: error:
    Failed to load interface for ‘Data.Text’
    It is a member of the hidden package ‘text-1.2.2.1’.
    Perhaps you need to add ‘text’ to the build-depends in your .cabal file.
    Use -v to see a list of the files searched for.

greet.hs:13:1: error:
    Failed to load interface for ‘Network.Wai’
    It is a member of the hidden package ‘wai-3.2.1.1’.
    Perhaps you need to add ‘wai’ to the build-depends in your .cabal file.
    Use -v to see a list of the files searched for.

greet.hs:14:1: error:
    Failed to load interface for ‘Network.Wai.Handler.Warp’
    It is a member of the hidden package ‘warp-3.2.9’.
    Perhaps you need to add ‘warp’ to the build-depends in your .cabal file.
    Use -v to see a list of the files searched for.

greet.hs:16:1: error:
    Failed to load interface for ‘Servant’
    It is a member of the hidden package ‘servant-server-0.9.1.1’.
    Perhaps you need to add ‘servant-server’ to the build-depends in your .cabal file.
    Use -v to see a list of the files searched for.

生成されたgreet.cabalファイルに、下記の部分を追加しました。バージョンは省いています。

 59   -- Other library packages from which modules are imported.
 60   build-depends:       base >=4.9 && <4.10,   -- カンマを追加
 61                        aeson,                 -- 追加
 62                        text,                  -- 追加
 63                        wai,                   -- 追加
 64                        warp,                  -- 追加
 65                        servant-server         -- 追加

実行します。

 foo$ cabal run greet.cabal 
./greet.cabal has been changed. Re-configuring with most recently used
options. If this fails, please run configure manually.
Resolving dependencies...
Configuring greet-0.1.0.0...
Preprocessing executable 'greet' for greet-0.1.0.0...
Running greet...
^C
 foo$ 

最終的なファイルは下記です。

gist.github.com

Servantのログ出力

@lotz さんの下記の記事があります。手元で動かそうとしたところ(2017/01/15時点)、記事が書かれた頃とライブラリのバージョンが変わっているため、そのままでは動作しませんでした。そこで、修正部分をメモしました。

qiita.com

修正のポイントは以下の2点です。

  • EitherTExceptTに変更します。
  • EitherTMonadLoggerインスタンスにするコードを削除します。(ExceptTMonadLoggerインスタンスになっているため)

作業手順

  • 適当な作業用のディレクトリを作ります。
  • 記事よりservant-logger-example.cabalをコピーします。
  • 記事よりapp/Main.hsをコピーします。
  • 修正をします(下記に差分のdiffをとりました)。
  • cabal installします。
  • cabal buildします。
  • dist/build/app/appを実行します。

修正の差分は次の通りです。

foo$ diff app/Main_before.hs app/Main.hs 
14c14
< import Control.Monad.Trans.Either
---
> import Control.Monad.Trans.Except
24,27c24
< instance MonadLogger m => MonadLogger (EitherT e m) where
<     monadLoggerLog a b c d = lift $ monadLoggerLog a b c d
< 
< loggingServer :: ServerT API (EitherT ServantErr (LoggingT IO))
---
> loggingServer :: ServerT API (ExceptT ServantErr (LoggingT IO))
35a33
>     Warp.run 8080 $ serve api server

修正をしないと、下記のようなコンパイルエラーとなります。

[1 of 1] Compiling Main             ( app/Main.hs, interpreted )

app/Main.hs:35:18: error:
    • Couldn't match type ‘EitherT ServantErr (LoggingT IO)’
                     with ‘Control.Monad.Trans.Except.ExceptT ServantErr (LoggingT IO)’
        arising from a functional dependency between:
          constraint ‘servant-0.9.1.1:Servant.Utils.Enter.Enter
                        (EitherT ServantErr (LoggingT IO) Text)
                        (Control.Monad.Trans.Except.ExceptT ServantErr (LoggingT IO)
                         :~> Control.Monad.Trans.Except.ExceptT ServantErr IO)
                        (Control.Monad.Trans.Except.ExceptT ServantErr IO Text)’
            arising from a use of ‘enter’
          instance ‘servant-0.9.1.1:Servant.Utils.Enter.Enter
                      (m a) (m :~> n) (n a)’
            at <no location info>
    • In the expression:
        (hoistNat (Nat runStdoutLoggingT)) `enter` loggingServer
      In an equation for ‘server’:
          server = (hoistNat (Nat runStdoutLoggingT)) `enter` loggingServer
      In the expression:
        do { putStrLn "Listening on port 8080";
             let server
                   = (hoistNat (Nat runStdoutLoggingT)) `enter` loggingServer;
             Warp.run 8080 $ serve api server }

このコンパイルエラーの解決には下記が参考になりました。

stackoverflow.com

Control.Monad.Logger

Log出力したいだけなのにさっぱりわからず。下記は動作するけどコーディングの考え方が間違っているはず。LoggingTはMonadXXXXのインスタンスになっているのに、なんでliftが必要なのかも謎。Haskellの独学は厳しい。。。。

Control.Monad.Logger

Monad Logger Sample