Haskellの1 == "x" が違法だなんて誰も言ってない!


前提

例えばJavaを例に上げると、全てのclassはObjectを継承しており、Objectがequalsメソッドを持つので
異なる型を比較(equals)できてしまいます。

class Foo {}
class Bar {}

public class Test {
    public static void main(String[] args) {
        System.out.println(new Foo().equals(new Bar()));
    }
}
// {output}
// false

これはインスタンスをアップキャストしたい際などには便利ですが、
私個人としては「ある値x,yが異なる型を持てば同じものではない(x != y)」というものを認めた方が
誤りが発生しにくいと考えています

それをおおよそ認めたとも捉えれられる言語の1つとしてHaskellがあります

Haskellは異なる型の比較(==)をコンパイルエラーで報告します。

-- CharとBoolの比較
main :: IO ()
main = print $ 'a' == True
-- /tmp/nvimkIYPkj/4.hs:2:23: error:
--     • Couldn't match expected type ‘Char’ with actual type ‘Bool’
--     • In the second argument of ‘(==)’, namely ‘True’
--       In the second argument of ‘($)’, namely ‘'a' == True’
--       In the expression: print $ 'a' == True

繰り返しになりますが、これはある種の安全性を担保します。

1 == "x"Trueになることを合法にする

 上記のHaskellの例を見た時に、もしかしたら貴方は「柔軟性に乏しい」と考えたかもしれません。

そんなことはないよ。

そんなことないことについて、1 == "x"の比較が型付けによってTrueに成りうるという事実を以て示したいと思います。
(主にGHC拡張を振りかざすことによって)

IsString + Num

{-# LANGUAGE OverloadedStrings #-}

import Data.String (IsString(..))

data Foo = Foo

instance IsString Foo where
  fromString _ = Foo

instance Num Foo where
  fromInteger _ = Foo

instance Eq Foo where
  _ == _ = True

default (Foo)

main :: IO ()
main = print $ 1 == "x"
-- {output}
-- True

 値1"x"を、単一の値を持つ型Fooに型付けしてしまうアプローチです。

 GHCにはOverloadedStringsという独自言語拡張があり、
これは通常"x""x" :: Stringという暗黙的型付けを行うルールを、
"x" :: IsString x => xという暗黙的型付けに変更します。

つまるところ"x" :: Fooという型付けが合法になります。

 文字列リテラルに関するOverloadedStringsと似たような、数値リテラルに対する暗黙的型付けルールを
Haskellはデフォルトで持ちます。

このルールは値11 :: Num a => aというような型(1)に型付けます。
ですので1 :: Fooという暗黙的型付けが合法になります。

 最後にdefault (Foo)1のような明示的型付きを成されていない数値リテラルを
デフォルトでFooに型付けるようにします。

 上記3つのルール

  • OverloadedStrings"x" :: IsString x => xに型付ける
  • Haskellのデフォルト挙動が1 :: Num a => aに型付ける
  • default (Foo)1 :: Fooに型付ける

によって1 == "x"(1 :: Foo) == ("x" :: IsString x => x)として型付けられる。
かつ==は左辺と右辺に同じ型を持つので
(1 :: Foo) == ("x" :: Foo)(==) :: Foo -> Foo -> Bool
を導くことができました

Num

 先程はFooに集約しましたが、しかしながらStringに集約することも可能です。

{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TypeSynonymInstances #-}

instance Num String where
  fromInteger _ = "x"

default (String)

main :: IO ()
main = print $ 1 == "x"
-- {output}
-- True

 これは章「IsString + Num」と全く同じことをしていますが、
ただし11 :: Stringに型付けています。

RebindableSyntax

 最後に1つ、ぶっ飛んだ例を紹介して終わります。
これについては、あまり型付けに関していません。

{-# LANGUAGE RebindableSyntax #-}

import Prelude (Integer, String, IO, print, ($), (==))

fromInteger :: Integer -> String
fromInteger _ = "x"

main :: IO ()
main = print $ 1 == "x"
-- {output}
-- True

 GHCのRebindableSyntax拡張は、Haskell標準が1という式をPrelude.fromInteger (1 :: Integer)という式に展開するという仕様について改変します。

Haskell標準は1Prelude.fromInteger (1 :: Integer)に展開しますが、
RebindableSyntaxが有効になっている場合は1fromInteger (1 :: Integer)に展開します。

つまり、とりあえず現在のスコープにあるfromIntegerIntegerを取ってきて使うというめっちゃ乱暴なものです

ですので1 == "x"がローカルのfromIntegerの定義を用いて"x" == "x"という簡素な式を導きます

Integerをローカルに定義して、そちらを使うようにしても面白いかもしれません。

参考ページ



  1. 実際は暗黙的型付けというよりは糖衣構文の展開だけど、「1Numインスタンスのいずれかに型付けられる」ということを導くのは同じはず