本当は [Haskell] リテラルのあれこれ(数値・リスト・文字列) というタイトルで、リストや文字列のリテラルについても触れるつもりでしたが、思ったより長くなってしまったのでとりあえず数値リテラルについて。
お手柔らかに (*´σー`)エヘヘ
はじめに
HaskellではC言語などのように浮動小数点型のリテラルを、3.0
のように明示的に小数点をつけて書く必要はありません。
以下のコードはコンパイルを通ります。
fooInt :: Int
fooInt = 3
fooInteger :: Int
fooInteger = 3
fooFloat :: Float
fooFloat = 3
fooDouble :: Double
fooDouble = 3
型推論がそれぞれの「3
」の型を Int
,Integer
,Float
,Double
に決定してくれるからですね。
では以下のコードはどうでしょう?
newtype MyInteger = MyInteger Integer
fooMyInteger :: MyInteger
fooMyInteger = 3
このコードはこのままではコンパイルを通りません。
ですが、少しコードを書き足すことでこのコードもコンパイルを通るようになります。
つまり、自分で定義した型についても組み込みの型と同じようなリテラルが使えるということです。
この記事では、
- 数値リテラルを自作の型で使う方法
- 数値リテラルを書きやすくする言語拡張
について説明します。
数値リテラル
A Gentle Introduction To Haskell(原文・日本語版)によると、整数リテラルと小数リテラルは、それぞれ以下のように解釈されます
foo = 3
-- ↓
foo = fromInteger 3
bar = -3
-- ↓
bar = negate (fromInteger 3)
baz = 3.0
-- ↓
baz = fromRational 3.0
これらの関数 fromInteger
, negate
および fromRational
はそれぞれ、Num
クラスとFractional
クラスで多相的に定義されているものです。
つまり、自作の型はこれらのクラスのインスタンスにすれば組み込み型と同じようなリテラルが使えます。
newtype MyInteger = MyInteger Integer
instance Num MyInteger where
fromInteger = MyInteger
fooMyInteger :: MyInteger
fooMyInteger = 3
上記のコードはコンパイルを通ります。
(面倒くさくて(+)
などの定義を省略しました。簡単に定義するには、後述のGeneralizedNewtypeDeriving
拡張を使ってください)
ためしに乱用してみる
Bool
をNum
クラスのインスタンスにすることで数値リテラルでBool
を表現してみます。
バグの元だから 絶対 マネしないでね。
instance Num Bool where
(+) = (||)
(*) = (&&)
abs = const True
signum = id
negate = not
fromInteger = (> 0)
正の数をTrue
、(+)
を論理和、(*)
を論理積で表現してみました。
ちょっと試してみます。
main = do
print (-1 + 1 :: Bool) -- False || True
print (-1 + (-1) :: Bool) -- False || False
print ( 1 * 1 :: Bool) -- True && True
print (-1 * (-1) :: Bool) -- False && False
True
False
True
True
なんだか、最後の出力が思っていたのと違いますね。
このようなことが起こる理由はNegativeLiterals
拡張の紹介と一緒に説明します。
負数リテラル(NegativeLiterals
拡張)
A Gentle Introduction To Haskell(原文・日本語版)によると、単項演算子"-
"は二項演算子の(-)
と同じ結合優先度を持ちます。
また、栄光のグラスゴーHaskellコンパイラ利用の手引[link]によるとデフォルトでは負数リテラル -3 はnegate (fromInteger 3)
のように展開されます。
つまり、-1 * (-1)
は-(1 * (-1))
と解釈され、
negate (fromInteger 1 * negate (fromInteger 1))
と展開されます。
これで先程の最後の出力がTrue
になった理由が理解できました。
NegativeLiterals
拡張を使うとこの展開の仕方を変更できます。
リテラルの -3 はこの拡張が有効になっている間はfromInteger (-3)
と展開されます。
よって、-1 * (-1)
は
(fromInteger (-1) * fromInteger (-1))
と展開されます。
これにより、先程の例の最後の出力はFasle
になります。
小数風整数リテラル(NumDecimals
拡張)
NumDecimals
拡張を使うと、整数リテラルを小数リテラルのように指数表示を使って書けるようになります。
main = print (1.25e5 :: Int)
125000
大きな整数を扱う必要があるときにはなかなか便利。
2進数リテラル(BinaryLiterals
拡張)
Haskellでは10進法、8進法、16進法の整数リテラルは以下のように書くことができます。
main = do
print 10
print 0o10
print 0x10
10
8
16
BinaryLiterals
拡張を使うことで、2進数の整数リテラルも書くことができるようになります。
{-# LANGUAGE BinaryLiterals #-}
main = do
print 0b10
2
Qiitaの色付けが対応してませんが、ちゃんと動きます。
Numのインスタンスにするのが面倒くさい(GeneralizedNewtypeDeriving
拡張)
GeneralizedNewtypeDeriving
拡張を使うことで自作の型を簡単にNum
クラスのインスタンスにできます。
この言語拡張は、newtype
宣言で型をラッピングするときに、もとの型がインスタンスであったようなクラスをderiving
節に書くことで、もとの型のインスタンス宣言を使えるようになるものです。[参考]
百聞は一見にしかず、ということで実際に使ってみましょう。
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
newtype MyInteger = MyInteger Integer deriving Num
fooMyInteger :: MyInteger
fooMyInteger = 3
このように、deriving
節にNum
を書くことで、Integer
型のNum
の実装がそのまま自作の型MyInteger
でも使えるようになり、数値リテラルも使えるようになります。
この言語拡張は既存のモナドをラッピングする目的でもよく使われるものです。
「型安全のために既存の型をラッピングしたいけど、必要なクラスのインスタンスにするのが面倒くさい」という場面は多いと思われますが、この言語拡張を使えばそれも解決します。
まとめ
- 数値リテラルには
fromInteger
,negate
,fromRational
関数が関係している。 -
NegativeLiterals
拡張で負数リテラルの展開の仕方を変えられる。 -
NumDecimals
拡張で整数リテラルにも指数表記が使える。 -
BinaryLiterals
拡張で2進数整数リテラルが使える。 -
newtype
宣言で型をラッピングするならGeneralizedNewtypeDeriving
拡張がオススメ。
数値リテラルを使いこなして、簡潔で型安全なコードを書きましょう!
あとがき
Bool
をNum
クラスにするやつは、NegativeLiterals
拡張の説明のために書いただけなので、絶対に 実用コードで真似しないでください(バグの温床になることうけあいです)
自作の型をNum
クラスにすることで数値リテラルを使えるようにする例としては algebraic-graphs というグラフ代数のライブラリが面白いです。頂点を1つしか持たないグラフを整数リテラルで書けるようにしています。
どうしてもやりたかったオマケ
newtype Int1984 = Int1984 Int
instance Num Int1984 where
(+) (Int1984 x) (Int1984 y) = Int1984 (x + y + 1)
fromInteger = Int1984 . fromInteger
instance Show Int1984 where
show (Int1984 x) = show x
ghci> 2 + 2 :: Int1984
5
P.S.
お手柔らかにつっこんでね (*´σー`)エヘヘ