前提
例えば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
NumとEqとIsStringのインスタンスだけでできると思います。 pic.twitter.com/DSMCmvAIVX
— caffeine propulsion (@iand675) 2018年1月23日
{-# 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はデフォルトで持ちます。
このルールは値1
を1 :: 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
」と全く同じことをしていますが、
ただし1
を1 :: 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)
という式に展開するという仕様について改変します。
-
7.3. Syntactic extensions
- 7.3.11. Rebindable syntax and the implicit Prelude import
Haskell標準は1
をPrelude.fromInteger (1 :: Integer)
に展開しますが、
RebindableSyntax
が有効になっている場合は1
をfromInteger (1 :: Integer)
に展開します。
つまり、とりあえず現在のスコープにあるfromInteger
とInteger
を取ってきて使うというめっちゃ乱暴なものです
ですので1 == "x"
がローカルのfromInteger
の定義を用いて"x" == "x"
という簡素な式を導きます
Integer
をローカルに定義して、そちらを使うようにしても面白いかもしれません。
参考ページ
-
7.3. Syntactic extensions
- 7.3.11. Rebindable syntax and the implicit Prelude import
プログラマの面接を行うことになった面接官がHaskellerに尋ねた。
— Make 生活リズム 正常 again (@mod_poppo) 2018年1月23日
「2+2は何になりますか?」
Haskellerはドアに鍵をかけ、型定義とそれに対するNumインスタンスを書きながら小声で尋ねた。
「2+2をいくつにしたいんです?」https://t.co/ePWU41mZDr pic.twitter.com/0uQR1HEVpl
みなさん、 2+2 のようなクソ2番煎じネタじゃなくて、代数的実数の方を拡散してください https://t.co/eSMVWbRyTo
— Make 生活リズム 正常 again (@mod_poppo) 2018年1月23日
NumとEqとIsStringのインスタンスだけでできると思います。 pic.twitter.com/DSMCmvAIVX
— caffeine propulsion (@iand675) 2018年1月23日
-
実際は暗黙的型付けというよりは糖衣構文の展開だけど、「
1
がNum
インスタンスのいずれかに型付けられる」ということを導くのは同じはず ↩