📝

GHC 9.12の新機能

2024/10/21に公開

GHC 9.12.1-alpha1が2024年10月16日にリリースされました。

そのうち、GHCupのprerelease channelでも使えるようになるのではないかと思います(Haskellの環境構築2023の「補遺:アルファ版・ベータ版のGHCを使う」も参考)。

この記事では、GHC 9.12の新機能を筆者の独断と偏見に基づき確認していきます。過去の類似の記事は

です。

この記事は網羅的な紹介記事とはなっていません。特に、筆者が詳しくないRTSやTemplate Haskell周りはカバーできていません。是非、公式のリリースノート類も参照してください:

GHC 9.12に入る機能

MultilineStrings拡張

複数行文字列リテラルがMultilineStrings拡張として実装されます。

従来のHaskellで複数行にわたって文字列リテラルを書くには、unlines 関数を使う方法や、ギャップ(文字列リテラル中のバックスラッシュで囲まれた空白が無視される機能)などがありました。

str1 = unlines
  [ "aaa"
  , "bbb"
  , "ccc"
  ]
-- -> "aaa\nbbb\nccc\n"

str2 = "aaa\n\
       \bbb\n\
       \ccc\n"
-- -> "aaa\nbbb\nccc\n"

一方、MultilineStrings拡張を使うと、ダブルクォート3つで複数行文字列リテラルを書けるようになります。

{-# LANGUAGE MultilineStrings #-}

str3 = """
       aaa
       bbb
       ccc
       """
-- -> "aaa\nbbb\nccc"

複数行文字列リテラルを実装する言語は色々ありますが、言語によって微妙に書き方が違ったりします。GHCに実装されたものの特徴を何点か挙げておきます。

  • 共通するインデントは削除される。
  • 先頭が改行であれば、改行 \n が1個削除される。
  • 末尾が改行であれば、改行 \n が1個削除される。
  • 入力ファイルの改行コードがCRLFであっても、文字列に埋め込まれる改行コードはLFとして扱われる(予定)
    • alpha1の段階ではこの挙動は実装されていませんが、正式版が出るまでに直る予定です。

このことがわかる例も載せておきます:

{-# LANGUAGE MultilineStrings #-}

str4 = """
       aaa
          bbb
       ccc

       """
-- -> "aaa\n   bbb\nccc\n"

str5 = """
       aaa
       bbb
          ccc\n
       """
-- -> "aaa\nbbb\n   ccc\n"

OrPatterns拡張

パターンマッチでは、複数の枝で同じ処理をしたいことがあります。例えば、次のコードを考えます:

data T = Foo | Bar | Baz

f :: T -> IO ()
f Foo = putStrLn "A"
f Bar = putStrLn "B"
f Baz = putStrLn "B" -- f Barと同じ!

ここでは、f Barf Baz で同じ処理をしたいとしましょう。ここでは同じ処理を2回書きました。

この例で「2回書く」以外の方法としては、「ワイルドカードパターン _ を使う」という方法もあります。

data T = Foo | Bar | Baz

f :: T -> IO ()
f Foo = putStrLn "A"
f _ = putStrLn "B"

しかし、ワイルドカードパターンを使うとパターンマッチ対象のデータ構築子を増やした時にコードの修正漏れが発生する可能性が上がります。つまり、T の定義が Foo | Bar | Baz | Bazz となった時に「警告やエラーが出たところを修正する」というやり方が通用しなくなります。

そこで、OrPatterns拡張です。これを使うと、セミコロン区切りで複数のパターンを書けるようになります:

{-# LANGUAGE OrPatterns #-}

data T = Foo | Bar | Baz

f :: T -> IO ()
f Foo = putStrLn "A"
f (Bar; Baz) = putStrLn "B"

曖昧さがない場合は、括弧を使わずに書くことも可能です:

{-# LANGUAGE OrPatterns #-}

g :: T -> IO ()
g x = case x of
        Foo -> putStrLn "A"
        Bar; Baz -> putStrLn "B"

レイアウトによるセミコロン挿入も有効です:

{-# LANGUAGE OrPatterns #-}

h :: T -> IO ()
h x = case x of
        Foo -> putStrLn "A"
        Bar
        Baz -> putStrLn "B"

一方で、関数定義のパターンマッチでは括弧は省略できません:

{-# LANGUAGE OrPatterns #-}

f :: T -> IO ()
f Foo = putStrLn "A"
f Bar; Baz = putStrLn "B" -- 不可
-- レイアウト規則的には
--   f Foo = putStrLn "A"
--   f Bar
--   Baz = putStrLn "B"
-- と書いたのと同じことになる(のでエラー)

NamedDefaults拡張:default宣言の一般化

Haskellでは、型の曖昧性が発生する場合があります。例えば、

main = print ((777 :: Integer) ^ 3)

の指数部の 3 の型はどうなるべきでしょうか?別の例として、

main = print (read "123")

というコードにおいて read する型はどうなるべきでしょうか?

Haskell 2010では、曖昧な型変数に Num 系の制約がついている場合に、default 宣言によってこれを解決することを可能にします。具体的には、

  • 型変数 v に対する制約が C v の形に限られること
  • 制約しているクラスの少なくとも数値系(Num またはそのサブクラス)であること
  • 制約しているクラスが全てPreludeか標準ライブラリーのクラスであること

という条件が満たされる場合に、

default (t1, ..., tn)

という形の default 宣言に記述された型を順番に試すようにします(defaulting)。何も書かなかった場合は

default (Integer, Double)

という default 宣言が有効なので、先の例の指数部の 3Integer に解決されます。一方、print (read ...) は数値系のクラスが絡まないのでエラーとなります。

GHCが拡張されるにつれて、このdefaultingに関する規則も拡張されてきました。例えば、GHCiではExtendedDefaultRulesという拡張が有効で、print (read ...) の例が通ります。OverloadedStrings拡張を使うと、IsString クラスにもdefaultingが働き、String 型がdefaultの候補に入ります。一方で、OverloadedLists拡張にはdefaultingは働きません。

NamedDefaults拡張では、default 宣言において

default C (t1, ..., tn)

のようにクラスを指定することができます。そして、defaultingが発動する条件は

  • 型変数 v に対する制約の中に C v の形のものが1つ以上あること

と緩和され、候補が default C の中から探索されます。該当するクラスが複数ある場合は、同じ候補に解決される必要があります。

また、モジュールから default 宣言をエクスポートすることもできるようになります。

詳細はGHC Proposalやドキュメントを見てください。

注意点として、GHC Proposalの例とは裏腹に、IsList に関しては実質使えないと思った方が良さそうです。要素を指定しないリスト [] 型は IsList のインスタンスではない(インスタンスとなるのは [Int] のように要素を指定した型)ので、

default IsList ([])

という宣言はできません。そして、要素型を指定してみても

{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE NamedDefaults #-}
import GHC.IsList

default IsList ([Char])

main = print ['a']

というコードは型推論の都合か何かでうまくいきません。

型宣言におけるワイルドカード _

Haskellでは、項レベルの関数が引数を受け取らない場合、変数名の代わりにワイルドカード _ を利用できます。

const :: a -> b -> a
const x _ = x

一方、これまでのGHCでは型レベル関数の引数には全て名前を与える必要がありました。

type Const x y = x -- OK
-- type Const x _ = x -- 不可

GHC 9.12ではTypeAbstractions拡張の一環で型レベル関数の引数にワイルドカード _ を使用できるようになります。

{-# LANGUAGE TypeAbstractions #-}

type Const x _ = x -- OK

HasField クラスとrepresentation polymorphism

GHCは、HasField クラスでレコードのフィールドにアクセスできる仕組みを持っています。例えば、GHC 9.2で追加されたOverloadedRecordDot拡張は、HasField クラスを使ってドット記法を脱糖しています。

HasField クラスは、従来は次のように定義されていました:

module GHC.Records where

class HasField (x :: k) r a | x r -> a where
  getField :: r -> a

x はフィールド名で、典型的には Symbol カインドの型です。r はレコードの型、a はフィールドの型です。

HasFieldOverloadedRecordDot の使用例は次のようになります:

{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE DataKinds #-}
import GHC.Records

instance HasField "successor" Int Int where
  getField x = x + 1

main :: IO ()
main = do
  print $ (37 :: Int).successor -- 37の次の整数(38)

さて、HasField クラスのカインドは、従来は k -> Type -> Type -> Constraint でした:

GHCi, version 9.10.1: https://www.haskell.org/ghc/  :? for help
ghci> :m + GHC.Records
ghci> :set -fprint-explicit-runtime-reps -fprint-explicit-kinds -XNoStarIsType
ghci> :k HasField
HasField :: k -> Type -> Type -> Constraint

このことは、レコードの型やフィールドの型としてunboxedな型やunliftedな型は使えないことを意味します。実際、次のコードはGHC 9.10ではコンパイルできませんでした:

{-# LANGUAGE MagicHash #-}
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE DataKinds #-}
import GHC.Exts
import GHC.Records

instance HasField "successor" Int# Int# where
  getField x = x +# 1#

main :: IO ()
main = do
  print $ I# (37# :: Int#).successor

この制限がGHC 9.12では緩和されます。GHC 9.12では HasField のカインドは次のようになります:

ghci> :m + GHC.Records
ghci> :set -fprint-explicit-runtime-reps -fprint-explicit-kinds -XNoStarIsType
ghci> :k HasField
HasField :: k -> TYPE r_rep -> TYPE a_rep -> Constraint

そして、Int# の例も通るようになります。

ちなみに、TYPE を使ってunboxedな型を統一的に扱えるようにする仕組みは当初はlevity polymorphismと呼ばれていましたが、これは今はrepresentation polymorphismと呼ばれています。GHC 9.2でlifted boxed←→unlifted boxedのみを統一的に扱う「本物の(?)levity polymorphism」(BoxedRep)が導入されたことによります。

RequiredTypeArguments拡張の強化(項の中に ->=> を書けるようになる)

GHC 9.10で導入されたRequiredTypeArguments拡張(参照:GHC 9.10で実装された可視なforallで遊ぶ)ですが、GHC 9.10の時点では関数などの矢印は項のレベルでは使えませんでした(type の明示が必要)。この制限が緩和され、->=>type なしで書いても型として扱えるようになりました。

{-# LANGUAGE RequiredTypeArguments #-}

id' :: forall a -> a -> a
id' _ x = x

main = do
  let f = id' (Int -> Int) (+ 5)
  -- GHC 9.10ではExplicitNamespaces拡張を使って
  -- let f = id' (type (Int -> Int)) (+ 5)
  -- と書く必要があった
  print $ f 37

Unboxed Float#/Double# のHexFloatLiterals

HexFloatLiterals拡張を使うと、浮動小数点数の十六進表記(参考:浮動小数点数の16進表記)ができるようになります。0x1.cafep100 みたいなやつです。

これがunboxedな Float#/Double# 型でも使えるようになりました。例:

{-# LANGUAGE MagicHash #-}
{-# LANGUAGE HexFloatLiterals #-}
import GHC.Exts

main :: IO ()
main = do
  print (F# 0x1.cafep0#)
  print (D# 0x1.cafep0##)

UnliftedFFITypes拡張の制限の緩和

UnliftedFFITypes拡張を使うと、unliftedな型をFFIで受け渡しできます。ByteArray# やSIMDの型のように、UnliftedFFITypesを使わないと受け渡しできない型もあります。

今回、空のタプルを引数として扱えるようになりました。例:

foreign import ccall unsafe foo :: (# #) -> Int32#

NCGバックエンドのRISC-V(64ビット)対応

RISC-Vは新興の命令セットアーキテクチャーで、組み込み方面から勢力を伸ばしています。スマホやパソコンの市場を置き換えるものになるかはわかりませんが、SBC(ラズパイみたいなやつ)は色々登場しています。

そういうわけで、GHCもRISC-Vへの対応を進めています。GHC 9.2ではLLVMバックエンドで64ビットRISC-Vに対応しました。

今回、NCG (Native Code Generator) が64ビットRISC-Vに対応して、LLVMなしでもビルドできるようになります。

現時点では公式からはビルド済みのRISC-V向けGHCは配布されていないので、RISC-V向けコード生成を試したかったら自前でビルドすることになるでしょう。GHCをクロスコンパイラーとしてビルド・インストールする手順は次のようになります:

$ # 依存関係のインストール(Ubuntuの場合)
$ sudo apt install build-essential curl autoconf gcc-riscv64-linux-gnu g++-riscv64-linux-gnu
$ sudo apt install qemu-user

$ # ghcupを使ってGHC(9.6以降)をインストールしておく
$ ghcup install ghc 9.6.6 --set
$ cabal install alex happy

$ GHC_VERSION=9.12.20241014
$ curl -LO https://downloads.haskell.org/~ghc/$GHC_VERSION/ghc-$GHC_VERSION-src.tar.xz && tar -xJf ghc-$GHC_VERSION-src.tar.xz
$ cd ghc-$GHC_VERSION
$ ./configure --target=riscv64-linux-gnu

$ # ビルド(時間がかかる)
$ hadrian/build --bignum=native -j binary-dist-dir

$ # 生成されたバイナリーのインストール
$ cd _build/bindist/ghc-$GHC_VERSION-riscv64-linux-gnu
$ ./configure --target=riscv64-linux-gnu --prefix=$HOME/ghc-rv64 CC=riscv64-linux-gnu-gcc CXX=riscv64-linux-gnu-g++
$ make install

この手順で動かなかったら適宜修正してください。現時点ではビルド済みバイナリーの configure 時にも色々設定する必要があるのがポイントです。

実行例は次のようになります:

$ echo 'main = putStrLn "Hello world!"' > hello.hs
$ ~/ghc-rv64/bin/riscv64-linux-gnu-ghc hello.hs
$ file hello
hello: ELF 64-bit LSB executable, UCB RISC-V, RVC, double-float ABI, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-riscv64-lp64d.so.1, BuildID[sha1]=250f432c120ef3948b7936b16a26b4add734ae69, for GNU/Linux 4.15.0, not stripped
$ qemu-riscv64 -L /usr/riscv64-linux-gnu/ ./hello
Hello world!

GHCが本格的に対応ということになってくると、RISC-Vの実機が欲しくなってきますね。

x86 NCGでのSIMDサポート

SIMDはsingle instruction, multiple dataの略で、一つの命令で複数のデータを処理できるCPUの機能のことです。

専用の命令を使うので、活用にはコンパイラー側の対応が必要です。具体的には、普通のループをコンパイラー側で書き換えてSIMD命令を活用する(自動ベクトル化)か、専用のデータ型と組み込み関数を用意してプログラマーにSIMD命令を活用させるか、です。

現状のGHCでのやり方は後者で、FloatX4# のようなデータ型と plusFloatX4# のような組み込み関数が用意されています。ただ、これまではこれらに対応しているのはLLVMバックエンドに限られており、一般のライブラリーで活用するにはハードルが高い状態でした。

今回、x86向けのNCGが一部のSIMDデータ型と組み込み関数に対応しました。具体的には、128ビット幅の浮動小数点数ベクトル、つまり FloatX4#DoubleX2# です。整数や256ビット以上には未対応です。また、LLVMではSSE2向けにコンパイルできるコードでもSSE 4.1を要求したりします。

とはいえ、実装のための面倒な部分(レジスターのスタックへの退避)が今回片付いたようなので、あとはやる気のある人が手を動かせば対応状況は改善していくのではないかと思います。私も暇があれば貢献するつもりです。

GHCのSIMDのサンプルコードも載せておきます。

{-# LANGUAGE MagicHash #-}
{-# LANGUAGE UnboxedTuples #-}
import GHC.Exts

main :: IO ()
main = do
  let v = packFloatX4# (# 1.1#, 2.2#, 3.3#, 4.4# #)
      w = packFloatX4# (# 0.1#, 0.2#, 0.3#, 0.4# #)
      x = minusFloatX4# v w
      (# a, b, c, d #) = unpackFloatX4# x
  print (F# a, F# b, F# c, F# d)

コンパイルには、今回新しく対応したx86 NCGで

$ ghc -msse4 simdtest.hs

とするか、従来から対応しているLLVMバックエンドで

$ ghc -fllvm simdtest.hs

とします。

SIMDを真面目に使うには何らかのラッパーライブラリーが欲しいところですが、Hackageにあるやつ(simdprimitive-simd)は最終更新日時が古く、使えるか不明です。誰かが新たに作るべきかもしれません。

SIMDプリミティブの追加

x86 NCGへのSIMDの実装と関連して、いくつかプリミティブが追加されました。例を挙げます:

module GHC.Prim where
fmaddFloatX4# :: FloatX4# -> FloatX4# -> FloatX4# -> FloatX4# -- x * y + z
fmsubFloatX4# :: FloatX4# -> FloatX4# -> FloatX4# -> FloatX4# -- x * y - z
fnmaddFloatX4# :: FloatX4# -> FloatX4# -> FloatX4# -> FloatX4# -- - x * y + z
fnmsubFloatX4# :: FloatX4# -> FloatX4# -> FloatX4# -> FloatX4# -- - x * y - z
shuffleFloatX4# :: FloatX4# -> FloatX4# -> (# Int#, Int#, Int#, Int# #) -> FloatX4#
minFloatX4# :: FloatX4# -> FloatX4# -> FloatX4#
maxFloatX4# :: FloatX4# -> FloatX4# -> FloatX4#

浮動小数点数のmin/maxも追加されました

module GHC.Prim where
minFloat# :: Float# -> Float# -> Float#
maxFloat# :: Float# -> Float# -> Float#

が、環境によって動作が違うので、将来仕様変更されるかもしれません(#25350: Floating-point min/max primops should have consistent behavior across platforms · Issues · Glasgow Haskell Compiler / GHC · GitLab)。

なお、新たに追加されたプリミティブは GHC.Exts からはエクスポートされません。ラッパーがないと普通のHaskellユーザーには縁遠いかもしれません。

Windows上で何もしなくてもLLVMバックエンドを使える

Haskellの環境構築2023では「Windows上にLLVMのツールを用意するのは厄介だ」というようなことを書きました。当時は opt.exellc.exe が公式の配布バイナリーに含まれなかったのです(今は含まれるようです)。しかも、何らかの方法でこれらを用意しても、浮動小数点数を使うとリンクエラーが出たりします。

今回、これらの問題が解決されて、Windows上で何もしなくてもLLVMバックエンドが使えるようになりました。つまり、opt.exellc.exe はGHCに付属のものが使われるようになり(実は少し前にWindows向けのGHCはClangを使うようになっており、LLVM自体は付属するようになっていたのでした)、浮動小数点数絡みのリンクエラーも解決しました。

ライブラリー

Data.List{.NonEmpty}.compareLength

Data.List.compareLength :: [a] -> Int -> Ordering
Data.List.NonEmpty.compareLength :: NonEmpty a -> Int -> Ordering

compare (length xs) n の安全で高速な代替物です。つまり、xs の要素を全て数える必要がありませんし、xs が無限リストでも使えます。

ghci> compareLength ['A','B','C'] 3
EQ
ghci> compareLength [0..] 3
GT

flip がrepresentation polymorphicになる

なりました。

ghci> :set -fprint-explicit-runtime-reps
ghci> :type flip
flip
  :: forall (repc :: GHC.Types.RuntimeRep) a b (c :: TYPE repc).
     (a -> b -> c) -> b -> a -> c

型引数が増えた結果、 flip に型適用する際の挙動が変わるので注意してください。

read が整数の二進表記に対応

しました。

ghci> read "0b1011" :: Integer
11
ghci> read "0b1011" :: Int
11

Data.List.{inits1,tails1}

module Data.List where

inits1 :: [a] -> [NonEmpty a]
tails1 :: [a] -> [NonEmpty a]

inits1 は「前から n 個取って作った部分列」のリストを返します。inits と異なり、n は1以上となります。

tails1 は「前の n 個を取り除いて作った部分列」のリストを返します。tails と異なり、n は1以上となります。

ghci> inits1 ["A","B","C","D"]
["A" :| [],"A" :| ["B"],"A" :| ["B","C"],"A" :| ["B","C","D"]]
ghci> tails1 ["A","B","C","D"]
["A" :| ["B","C","D"],"B" :| ["C","D"],"C" :| ["D"],"D" :| []]

Data.Bitraversable.{firstA,secondA}

module Data.Bitraversable where

firstA :: (Bitraversable t, Applicative f) => (a -> f c) -> t a b -> f (t c b)
secondA :: (Bitraversable t, Applicative f) => (b -> f c) -> t a b -> f (t a b)

Bitraversable は要素型が2つある Traversable みたいなやつです(たぶん)。標準ライブラリーの中では Either やタプル (,) がインスタンスとなります。

Bitraversable

bitraverse :: (Bitraversable t, Applicative f) => (a -> f c) -> (b -> f d) -> t a b -> f (t c d)

というメソッドを持っており、これを特殊化したものが今回追加された firstAsecondA と言って良さそうです。

おまけ:私の貢献

私(@mod_poppo)が行なった貢献(バグ報告や修正など)で、GHC 9.12に入るものを備忘録代わりに書いておきます。x86 NCGにSIMDを実装するやつに感化された活動がちょいちょいあります。

これらの貢献は趣味として、無償でやっています。私を支援したいと思った方には、Zennでバッジを送る、「だめぽラボ」の同人誌を買う、GitHub Sponsorsで支援するなどの手段があります。

自分でもGHCに貢献してみたい、という人は「GHCへの私の貢献2023」に書いたことも参考にしてください。まずはGitLabを眺めて雰囲気を掴むのが良いでしょうか。アカウント作成はスパム対策の関係で人手での承認が必要なのがトリッキーです。

Discussion