Help us understand the problem. What is going on with this article?

[Haskell] 数値リテラルの活用

More than 1 year has passed since last update.

本当は  [Haskell] リテラルのあれこれ(数値・リスト・文字列)  というタイトルで、リストや文字列のリテラルについても触れるつもりでしたが、思ったより長くなってしまったのでとりあえず数値リテラルについて。

お手柔らかに (*´σー`)エヘヘ

はじめに

HaskellではC言語などのように浮動小数点型のリテラルを、3.0のように明示的に小数点をつけて書く必要はありません。
以下のコードはコンパイルを通ります。

literal
fooInt :: Int
fooInt = 3

fooInteger :: Int
fooInteger = 3

fooFloat :: Float
fooFloat = 3

fooDouble :: Double
fooDouble = 3

型推論がそれぞれの「3」の型を Int,Integer,Float,Double に決定してくれるからですね。

では以下のコードはどうでしょう?

literalNewtype
newtype MyInteger = MyInteger Integer

fooMyInteger :: MyInteger
fooMyInteger = 3

このコードはこのままではコンパイルを通りません。

ですが、少しコードを書き足すことでこのコードもコンパイルを通るようになります。
つまり、自分で定義した型についても組み込みの型と同じようなリテラルが使えるということです。

この記事では、

  • 数値リテラルを自作の型で使う方法
  • 数値リテラルを書きやすくする言語拡張

について説明します。

数値リテラル

A Gentle Introduction To Haskell(原文日本語版)によると、整数リテラルと小数リテラルは、それぞれ以下のように解釈されます

literal
foo = 3
-- ↓
foo = fromInteger 3

bar = -3
-- ↓
bar = negate (fromInteger 3)

baz = 3.0
-- ↓
baz = fromRational 3.0

これらの関数 fromInteger, negate および fromRational はそれぞれ、NumクラスとFractionalクラスで多相的に定義されているものです。

つまり、自作の型はこれらのクラスのインスタンスにすれば組み込み型と同じようなリテラルが使えます。

literalNewtype
newtype MyInteger = MyInteger Integer

instance Num MyInteger where
  fromInteger = MyInteger

fooMyInteger :: MyInteger
fooMyInteger = 3

上記のコードはコンパイルを通ります。
(面倒くさくて(+)などの定義を省略しました。簡単に定義するには、後述のGeneralizedNewtypeDeriving拡張を使ってください)

ためしに乱用してみる

BoolNumクラスのインスタンスにすることで数値リテラルでBoolを表現してみます。

バグの元だから 絶対 マネしないでね。

bool.hs
instance Num Bool where
  (+) = (||)
  (*) = (&&)
  abs = const True
  signum = id
  negate = not
  fromInteger = (> 0)

正の数をTrue(+)を論理和、(*)を論理積で表現してみました。
ちょっと試してみます。

bool.hs
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
output
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拡張を使うと、整数リテラルを小数リテラルのように指数表示を使って書けるようになります。

numDecimals
main = print (1.25e5 :: Int)
output
125000

大きな整数を扱う必要があるときにはなかなか便利。

2進数リテラル(BinaryLiterals拡張)

Haskellでは10進法、8進法、16進法の整数リテラルは以下のように書くことができます。

literals.hs
main = do
  print 10
  print 0o10
  print 0x10
output
10
8
16

BinaryLiterals拡張を使うことで、2進数の整数リテラルも書くことができるようになります。

literals.hs
{-# LANGUAGE BinaryLiterals #-}
main = do
  print 0b10
output
2

Qiitaの色付けが対応してませんが、ちゃんと動きます。

Numのインスタンスにするのが面倒くさい(GeneralizedNewtypeDeriving拡張)

GeneralizedNewtypeDeriving拡張を使うことで自作の型を簡単にNumクラスのインスタンスにできます。

この言語拡張は、newtype宣言で型をラッピングするときに、もとの型がインスタンスであったようなクラスをderiving節に書くことで、もとの型のインスタンス宣言を使えるようになるものです。[参考]

百聞は一見にしかず、ということで実際に使ってみましょう。

generalized.hs
{-# 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拡張がオススメ。

数値リテラルを使いこなして、簡潔で型安全なコードを書きましょう!

あとがき

BoolNumクラスにするやつは、NegativeLiterals拡張の説明のために書いただけなので、絶対に 実用コードで真似しないでください(バグの温床になることうけあいです)

自作の型をNumクラスにすることで数値リテラルを使えるようにする例としては algebraic-graphs というグラフ代数のライブラリが面白いです。頂点を1つしか持たないグラフを整数リテラルで書けるようにしています。

どうしてもやりたかったオマケ

Int1984.hs
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
ghci> 2 + 2 :: Int1984
5

P.S.
お手柔らかにつっこんでね (*´σー`)エヘヘ

tezca686
ぼちぼち記事を書きます。 お手柔らかに
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした