2017/12/25
にこ、希と一緒に学ぶHaskell(番外)「あまり知られていないGHC拡張の紹介」

 この記事はHaskell (その3) Advent Calendar 2017の 21日目の記事です!

 この記事にはSS表現、ラブライブが含まれます。 これらが苦手な方はブラウザバックを推奨します。


Outline

前回の型ライブ!

放課後、部室に集まって、よく知られたGHC拡張について語り合う にこっち と うち。

希「GHC拡張っていっぱいあるんやね!」
にこ「Haskell reportに『実験的な機能を取り入れた、この言語の拡張や 変形の出現はのぞむところである。』って書いてあるくらいだしね」

調べれば調べるほど出てくるGHC拡張に対して、改めて全能感を感じる。
でも、Haskellの力はまだまだこんなもんやない。

そんな充実感を抱きながらお風呂から上がると、うちのGoogleHangoutに、謎のビデオ電話がかかってきた!

にこののぞみ

希「はい、もしもし」
にこ「こんばんは、希」
希「にこっちやん。どうしたの?」

にこ「そのね、今日は珍しくママ……じゃなくてっ!」

にこ「お母さんが早く帰ってきてて、チビたちの面倒を見てくれてるから暇なんだけど、 Haskellのリモート勉強会しない? ……なんて。」
希「おっ、ええやん! うちも実は、今ひまやったんよ」

にこ「そ、そう! じゃ、じゃあ…… 放課後だけじゃあんまり語り尽くせなかったし、 またGHC拡張について話さない?」

希「いいね! 実はさっき面白いページ見つけてな。 知らないGHC拡張がいっぱいあったんよ。 これ見ていくの、どうやろ?」

にこ「ええ、いいわねっ」

MultiParamTypeClasses

希「さっき見つけたのが面白いのがあるんよ」

希「MultiParamTypeClassesについては、にこっち多分知っとると思うんやけど」
にこ「ええ」
希「これって、0個の型に対するインスタンスも定義できるらしいやね」

{-# LANGUAGE MultiParamTypeClasses #-}

class Nullary
instance Nullary

main :: IO ()
main = return ()

にこ「!? 型クラスの受け取る型を、1個以上…… じゃなくて0個以上として一般化する拡張だったのね……」

にこ「これ、何にどうやって使うのかしら」
希「うーん、わからへん……」

ConstrainedClassMethods

にこ「なにこれ、しらないわ」
希「型クラスの関数に、型制約を設けることができるようになるみたいやね」

{-# LANGUAGE ConstrainedClassMethods #-}

class UnitedConvertible a where
  toItself :: a -> a
  -- ConstrainedClassMethods allows to restrict constraints in the type class function
  toString :: Show a => a -> String

main :: IO ()
main = return ()

希「Haskell98は型クラス関数に型制約を設けることを禁止してるらしくって」
希「それの回避策らしいみたい。 Haskell98がそれを禁止してるの、なんでやろな?」
にこ「Haskell reportは実装に言及しないらしいし、実装都合じゃあなさそうね。 なんでかしら」

にこ「ていうか、型クラスの関数って、型制約付けられなかったのね。 型クラス関数に他の型クラスの制約をつけることなんてあまりないものね……」

RebindableSyntax

にこ「名前からして面白そうね」
希「やね!ふむふむこれは…… Haskel reportでは以下の変換規則が規定されているらしいんやけど」

1

Prelude.fromInteger (1 :: Integer)

希「それを以下の変換規則に付け替えるんやって。」

1

fromInteger (1 :: Integer)

にこ「ふむふむ、Preludeの定義を参照はずのものを、ローカルの定義への参照に変えられるのね」

にこ「うわっ、putStrLn 1式を合法にできた……」

{-# LANGUAGE RebindableSyntax #-}

-- RebindableSyntax turns on NoImplicitPrelude
import Prelude hiding (fromInteger)

-- In the Haskell report specification,
-- a literal `1` is expanded to `Prelude.fromInteger (1 :: Integer)`.
--
-- RebindableSyntax changes it to that is expanded to `fromInteger (1 :: Integer)`,
-- also it means `1`'s convertion is able to rebind.
fromInteger :: Integer -> String
fromInteger _ = ";P"

main :: IO ()
main = putStrLn 1
-- {output}
-- ;P

希「putStrLn 1、字面がすごいわ……。 ああこれ、NoImplicitPreludeも有効になるんね」

希「他にはこんな変換規則を付け替えられるみたいやね」

252.52 Prelude.fromRational (252.52 :: Rational)
x == y x Prelude.== y
x - y x Prelude.- y
x >= y x Prelude.>= y
- x negate x
if x then y else z ifThenElse x y z
#nozo (if OverloadedLabels is used) fromLabel @ "nozo"
リスト色々 OverloadedLists - GHC User’s Guide

希「あとはdo, mdo, リスト内包記とArrowsについても変換規則があるみたいやけど、明記はされてないんね。 doは多分こんな感じかなあ。 本当は多分パターンマッチまで考慮してそうやね」

do
  x' <- x
  y
  z

x Prelude.>>= \x' ->
y Prelude.>> z

NoImplicitPrelude(補足)

にこ・希(読んでの通り、暗黙的にimport Preludeを行わないための拡張。 これを指定しない限り、GHCは暗黙的にそれを行う)

InstanceSigs

にこ「これは、インスタンスを定義するときに、型クラスの関数の具体的シグネチャをインスタンス側で書けるやつね」
希「さすがにこっちやね、知っとったか」

{-# LANGUAGE InstanceSigs #-}

class Identical a where
  id' :: a -> a

instance Identical Int where
  -- InstanceSigs allows this type declaration
  id' :: Int -> Int
  id' = id

main :: IO ()
main = return ()

にこ「ええ。 インスタンス側の型クラス関数が、そのときに使いたい型になっているか不安なときに使ったりするわ。 ScopedTypeVariablesと一緒に使うと、より厳格に型検査をしてもらえるのよね」

{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE ScopedTypeVariables #-}

newtype List' a = List'
  { unList' :: [a]
  }

-- ランダムなリストを生成するインスタンス
instance (Bounded a, Random a) => Random (List' a) where
  randomR :: forall g. RandomGen g => (List' a, List' a) -> g -> (List' a, g)
  randomR (List' xs, List' ys) gen =
    let randomR' :: a -> a -> (a, g)  -- 意図通りの関数を書けてるか、型検査
        randomR' = curry $ flip randomR gen
        zs' :: [(a, g)]  -- 意図通りの結果を受け取れてるか、型検査
        zs' = zipWith randomR' xs ys
    in ...

にこ「でも、一度コンパイルが通ったら消しちゃうことも少なくないわ…… コンパイル速度が落ちそうな気がしちゃって」

希「どうなんやろな?」

ImpredicativeTypes

にこ「非可述的な……型々?」
希「なんやろこれ??」

にこ「こういう時はここを見るのが一番ね。 あったあった、GHC Wikiページ!」

{-# LANGUAGE ImpredicativeTypes #-}
{-# LANGUAGE RankNTypes #-}

f :: Bool -> b -> b
f = const id

g :: (forall a. a -> a) -> Int
g = undefined --const 10

foo :: Bool -> Int
foo x = g $ f x

--foo' :: Bool -> Int
--foo' = g . f

main :: IO ()
main = return ()

にこ「なるほど、うーん。 多分、fの型変数bと、gランク2多相の型変数aが、同一のものとして具体化できるか というのが問題っぽいわね」

希「ImpredicativeTypesを使うと、こんな感じの型変数xが決定できる?」

      f
Bool --> (x -> x)
    \     |
     \    | g
      \   v
       > Int

にこ「うーん、そうっぽい?」
希「うーん」

にこ「gの実定義をg = const 10にしたら、 ImpredicativeTypesを外してもコンパイルが通るわ」
にこ「逆にfoo'をコメントインすると、ImpredicativeTypesを有効にしていてもコンパイルが通らないわ」

希「うーん、さらにいろいろ機能があるみたいやね」
希「ここが参考になりそうやん」

ExplicitNamespaces

にこ「ああ、型演算子を作った時に、 importとmoduleでのエクスポートでtypeって書けるのは この子のおかげだったのね」

Helper.hs

{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}

module Helper
  ( type (-<|)
  , (-<|)
  ) where

type family (-<|) a b where
  x -<| _ = x

(-<|) :: a -> b -> a
(-<|) = const

Main.hs

{-# LANGUAGE ExplicitNamespaces #-}

-- `ExplicitNamespaces` allows 'type' keyword in import/export
import Helper (type (-<|), (-<|))

x :: (-<|) Int Char
x = 10 -<| 'a'

main :: IO ()
main = return ()

希「このtypeってなんなん?」

にこ「このHelper.hsで、(-<|)は型レベルと値レベルのどちらでも定義されてるでしょ?」
にこ「それだとエクスポート時にmodule Helper ((-<|)) whereって書いたときと、 import時にimport Helper ((-<|))って書いたとき」
にこ「それぞれどちらをインポート、エクスポートしたらいいかわからないわよね」

にこ「だからtypeを指定したときには、型レベルの方を表すの」

希「なるほどな〜。 デフォルトで値レベルの(-<|) :: a -> b -> a を、 typeを指定したときには型レベルの(-<|)を扱うって感じやんな!」

夜更け

希「はぁ〜ふぅ」あくび

にこ(……もう24時ね)   にこ「眠い?」
希「うーん、ちょっと眠くなってもうたわ」

にこ「明日眠くなって調子でなかったら大変だし、今日はもう寝ましょうか」
希「せやね〜」

希「楽しかったよ〜。 普段はにこっちから『遊ぼう』って言ってくれることなんてないし」
にこ「悪かったわね。 アイドルに子育てにHaskell。 にこは忙しいのよ」

にこ「でも今日はありがとう、私も楽しかったわ」
希「うんん、いつでも呼んでな。 うち、Haskellerとしてのにこっちのことも、尊敬してるんよ」
にこ「……//」

参考ページ


この記事はこちらから修正リクエストを送ることができます。
にこ、希と一緒に学ぶHaskell(番外)「あまり知られていないGHC拡張の紹介」 - github
ゴミ箱ボタンの左にある、鉛筆ボタンを押してね!