前提
例えば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インスタンスのいずれかに型付けられる」ということを導くのは同じはず ↩