Ikhtisar Singkat dari Template yang Diketik Haskell

Selamat datang di posting kedua kami di Template Haskell!

Hari ini kita akan melihat sekilas Template Haskell yang diketik. Artikel ini mengasumsikan beberapa keakraban dengan Template Haskell (TH). Jika ini adalah perjalanan pertama Anda dengan TH, lihat pengantar kami tentang Template Haskell terlebih dahulu.

Untuk artikel ini, kita akan menggunakan GHC 8.10.4.

Mengapa mengetik TH?

Mengetik TH, seperti namanya, memungkinkan kami untuk memberikan jaminan statis yang lebih kuat tentang kebenaran meta-program. Dengan TH yang tidak diketik, ekspresi yang dihasilkan akan diperiksa tipenya saat disambung, yaitu selama penggunaannya, bukan definisinya. Dengan TH yang diketik, ekspresi tersebut sekarang diperiksa tipenya di situs definisinya.

Seperti halnya hal lain dalam ilmu komputer, ada kelebihan dan kekurangan dalam menggunakan TH yang diketik dibandingkan dengan TH biasa, beberapa di antaranya tercantum di bawah ini.

Keuntungan:

  • Jaminan keamanan tipe yang lebih besar.
  • Kesalahan tidak akan ditunda sampai digunakan; sebaliknya, mereka dilaporkan pada definisi mereka.

Kekurangan:

  • Harus digunakan dengan [|| ||] kutipan.
    • Ini berarti bahwa kita tidak dapat dengan mudah menggunakan Exp konstruktor secara langsung.
    • Sebagai perbandingan, untuk kutipan yang tidak diketik, kita bisa menggunakan [| |] atau langsung hubungi Exp konstruktor.
    • Atau, Anda dapat menggunakan unsafeCodeCoerce untuk mengatasinya, jika Anda ingin menggunakan fungsi yang tidak aman.
  • Hanya mendukung versi yang diketik dari Exp (tidak ada versi yang diketik untuk Dec, Pat, dll).
    • Tutorial TH kami sebelumnya tidak dapat ditulis murni dengan Typed TH, karena banyak digunakan Dec, Misalnya.
  • Mengharuskan jenis yang digunakan diketahui terlebih dahulu, yang dapat membatasi jenis program TH yang dapat Anda buat.

Sebelum kita mulai, pastikan Anda memiliki template-haskell paket diinstal, serta TemplateHaskell ekstensi bahasa diaktifkan.

>>> :set -XTemplateHaskell
>>> import Language.Haskell.TH

Ekspresi yang diketik

Dalam tutorial kami sebelumnya, kami belajar bahwa kami dapat menggunakan [e|...|] kutipan (yang sama dengan [|...|]) untuk membuat ekspresi tipe Q Exp. Dengan mengetik TH, kita akan menggunakan [e||...||] (yang sama dengan [||...||]) untuk membuat ekspresi tipe Q (TExp a).

Apa TExp a, Anda mungkin bertanya-tanya? Ini hanya sebuah newtype membungkus familiar kita Exp:

type role TExp nominal
newtype TExp (a :: TYPE (r :: RuntimeRep)) = TExp
  { unType :: Exp
  }

Arti dari TYPE (r :: RuntimeRep) part tidak penting bagi kami, tetapi sederhananya, ini memungkinkan GHC untuk menggambarkan bagaimana merepresentasikan beberapa jenis (box, unboxed, dll) selama runtime. Untuk informasi lebih lanjut, lihat polimorfisme kesembronoan.

Ini memungkinkan kita untuk menggunakan konstruksi yang kita kenal untuk Exp, selain jenis untuk a yang mewakili jenis ekspresi. Ini memberi kami mekanisme keamanan tipe yang lebih kuat untuk aplikasi TH kami, yang akan menyebabkan kompiler menolak program TH yang tidak valid selama konstruksinya.

Pada contoh di bawah ini, template-haskell dengan senang hati menerima 42 :: String menggunakan ekspresi yang tidak diketik, sedangkan mitra yang diketik menolaknya dengan kesalahan tipe.

>>> runQ [|42 :: String|]
SigE (LitE (IntegerL 42)) (ConT GHC.Base.String)

>>> runQ [||42 :: String||]
<interactive>:358:9: error:
    • Could not deduce (Num String) arising from the literal ‘42’
      from the context: Language.Haskell.TH.Syntax.Quasi m
        bound by the inferred type of
                   it :: Language.Haskell.TH.Syntax.Quasi m => m (TExp String)
        at <interactive>:358:1-23In the Template Haskell quotation [|| 42 :: String ||]
      In the first argument of ‘runQ’, namely ‘[|| 42 :: String ||]’
      In the expression: runQ [|| 42 :: String ||]

Sambungan yang diketik

Sama seperti kami memiliki sambungan yang tidak diketik seperti $foo, sekarang kami juga telah mengetik splices, ditulis sebagai $$foo. Namun, perhatikan bahwa jika versi GHC Anda di bawah 9.0, Anda mungkin perlu menulis $$(foo) sebagai gantinya.

Contoh: menghitung bilangan prima

Sebagai contoh, mari kita perhatikan fungsi-fungsi berikut yang menerapkan evaluasi bilangan prima hingga beberapa bilangan. Kami akan membuat dua versi, satu dengan Haskell biasa, dan satu lagi dengan Haskell Template, sehingga kami dapat melihat perbedaan di antara keduanya. Implementasinya mungkin agak lebih bertele-tele daripada yang diperlukan untuk mendemonstrasikan teknik dalam mengetik TH dan membedakannya dengan fungsi biasa.

Pertama, buat file Primes.hs berisi dua fungsi: satu yang memeriksa apakah suatu bilangan yang diberikan adalah bilangan prima, dan yang lain yang menghasilkan bilangan prima hingga batas tertentu.

module Primes where

isPrime :: Integer -> Bool
isPrime n
  | n <= 1    = False
  | n == 2    = True
  | even n    = False  
  | otherwise = go 3
  where
    go i
      | i >= n         = True  
      | n `mod` i == 0 = False
      | otherwise      = go (i + 2)  

primesUpTo :: Integer -> [Integer]
primesUpTo n = go 2
  where
    go i
      | i > n     = []
      | isPrime i = i : go (i + 1)
      | otherwise = go (i + 1)

Fungsi pertama memeriksa apakah suatu bilangan memiliki pembagi. Jika memiliki pembagi (selain dari 1 dan dirinya sendiri), maka bilangan tersebut adalah komposit dan fungsinya kembali False, jika tidak, ia terus menguji lebih banyak pembagi. Jika kita mencapai angka yang lebih besar atau sama dengan input, itu berarti bahwa kita telah memeriksa semua angka yang lebih kecil dan tidak menemukan pembagi, sehingga bilangan prima, dan fungsi kembali True.

Fungsi kedua hanya mengulangi angka-angka, mengumpulkan semua bilangan prima. Kita mulai dengan 2 karena ini adalah bilangan prima pertama.

Perlu diingat bahwa fungsi-fungsi ini adalah sangat tidak efisien, jadi pastikan untuk menggunakan versi yang lebih optimal untuk hal yang serius!

Sekarang untuk versi Template Haskell kami. Seperti biasa, mari kita buat dua file, TH.hs dan Main.hs, untuk bekerja dengan melalui contoh ini.

Inilah yang seharusnya ada di TH.hs:

{-# LANGUAGE TemplateHaskell #-}

module TH where

import Language.Haskell.TH

import Primes (isPrime)

primesUpTo' :: Integer -> Q (TExp [Integer])
primesUpTo' n = go 2
  where
    go i
      | i > n     = [||[]||]
      | isPrime i = [||i : $$(go (i + 1))||]
      | otherwise = [||$$(go (i + 1))||]

Secara umum, itu sama dengan versi biasa. Satu-satunya perbedaan sekarang adalah kami kembali Q (TExp [Integer]) dan buat daftar kami di dalam kutipan ekspresi yang diketik.

Kami membungkus panggilan rekursif kami ke go sambungan dalam. Sejak go memiliki jenis Q (TExp [Integer]), jika kami tidak menyambungkannya, kami akan mencoba menggunakan operator kontra (:) pada Integer dan Q (TExp [Integer]) yang tidak akan mengetik-periksa. Pesan kesalahan mungkin menjelaskan masalah dengan cukup baik:

>>> :l TH
[2 of 2] Compiling TH               ( TH.hs, interpreted )
Failed, no modules loaded.
TH.hs:15:21: error:
    • Couldn't match typeQ (TExp [Integer])’ with ‘[Integer]’
      Expected type: Q (TExp [Integer])
        Actual type: Q (TExp (Q (TExp [Integer])))In the Template Haskell quotation [|| (go (i + 1)) ||]
      In the expression: [|| (go (i + 1)) ||]
      In an equation for ‘go’:
          go i
            | i > n = [|| [] ||]
            | isPrime i = [|| i : $$(go (i + 1)) ||]
            | otherwise = [|| (go (i + 1)) ||]
   |
15 |       | otherwise = [||(go (i + 1))||]
   |                     ^^^^^^^^^^^^^^^^^^

Faktanya, kita bisa menulis cabang itu di atas hanya sebagai go (i + 1), tanpa kutipan. Cobalah!

Dan sekarang kita dapat menggunakan fungsi baru kita di GHCI seperti:

>>> $$(primesUpTo' 100)
[2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97]

Kami juga dapat memeriksanya sebagai definisi Template Haskell yang tidak diketik jika kami mau, dengan menggunakan unType fungsi:

>>> runQ (unType <$> primesUpTo' 10)
InfixE (Just (LitE (IntegerL 2))) (ConE GHC.Types.:) (Just (InfixE (Just (LitE (IntegerL 3))) (ConE GHC.Types.:) (Just (InfixE (Just (LitE (IntegerL 5))) (ConE GHC.Types.:) (Just (InfixE (Just (LitE (IntegerL 7))) (ConE GHC.Types.:) (Just (ConE GHC.Types.[]))))))))

Atau, lebih sederhananya:

2 : 3 : 5 : 7 : []

Apakah kita membuat kesalahan dalam definisi, misalnya, dengan menggunakan definisi berikut di mana kita melupakan panggilan rekursif:

primesUpTo' :: Integer -> Q (TExp [Integer])
primesUpTo' n = go 2
  where
    go i
      | i > n     = [||[]||]
      | isPrime i = [||i||]  
      | otherwise = go (i + 1))

Kemudian kita akan langsung disambut dengan kesalahan ketik:

>>> :r
[2 of 2] Compiling TH               ( TH.hs, interpreted )
Failed, no modules loaded.
TH.hs:14:21: error:
    • Couldn't match typeInteger’ with ‘[a]’
      Expected type: Q (TExp [a])
        Actual type: Q (TExp Integer)In the Template Haskell quotation [|| i ||]
      In the expression: [|| i ||]
      In an equation for ‘go’:
          go i
            | i > n = [|| [] ||]
            | isPrime i = [|| i ||]
            | otherwise = [|| $$(go (i + 1)) ||]
    • Relevant bindings include
        go :: Integer -> Q (TExp [a]) (bound at TH.hs:43:5)
   |
14 |       | isPrime i = [||i||]
   |                     ^^^^^^^

Kita primesUpTo' akan menghasilkan daftar bilangan prima pada waktu kompilasi, dan sekarang kita dapat menggunakan daftar ini untuk memeriksa nilai saat runtime.

Dengan ini, kita dapat membuat Main.hs, di mana kita dapat mencoba kode kita:

{-# LANGUAGE TemplateHaskell #-}

import TH

main :: IO ()
main = do
  let numbers = $$(primesUpTo' 10000)
  putStrLn "Which prime number do you want to know?"
  input <- readLn  
  if input < length numbers
    then print (numbers !! (input - 1))
    else putStrLn "Number too big!"

Dan itu saja! Program yang sangat sederhana menggunakan TH yang diketik. Memuat Main.hs di GHCI, dan setelah beberapa detik saat dimuat, jalankan main fungsi. Setelah dimintai masukan, ketikkan angka seperti 200, mintalah bilangan prima ke-200. Fungsi harus menampilkan hasil yang benar dari 1223.

>>> main
Which prime number do you want to know?
200
1223

Sekali lagi, algoritme kami cukup tidak efisien dan ini mungkin memerlukan beberapa detik untuk dikompilasi (karena menghasilkan angka saat dikompilasi), dan untuk perbaikan lebih lanjut, mungkin ide yang baik untuk memiliki algoritme yang kurang naif untuk menghasilkan bilangan prima, tetapi untuk tujuan pendidikan , itu akan dilakukan untuk saat ini.

Kode yang digunakan dalam posting ini juga dapat ditemukan di GitHub Gist ini.

Implementasi yang lebih singkat

Seperti disebutkan sebelumnya, kita dapat mengimplementasikan fungsi-fungsi di atas dengan cara yang lebih sederhana, seperti:

primesUpTo :: Integer -> [Integer]
primesUpTo n = filter isPrime [2 .. n]

Dan fungsi TH yang sesuai sebagai:

primesUpTo' :: Integer -> Q (TExp [Integer])
primesUpTo' n = [|| primesUpTo n ||]

Dan dengan ini, Anda harus siap menggunakan Template Haskell yang diketik di alam liar.

Peringatan

Template yang Diketik Haskell mungkin mengalami beberapa kesulitan dalam menyelesaikan kelebihan beban. Anehnya, berikut ini tidak mengetik-periksa:

>>> mempty' :: Monoid a => Q (TExp a)
... mempty' = [|| mempty ||]

>>> x :: String
... x = id $$(mempty')
<interactive>:549:11: error:
    • Ambiguous type variable ‘a0’ arising from a use of ‘mempty'’
      prevents the constraint ‘(Monoid a0)’ from being solved.
      Probable fix: use a type annotation to specify what ‘a0’ should be.
      These potential instances exist:
        instance Monoid a => Monoid (IO a) 
        instance Monoid Ordering 
        instance Semigroup a => Monoid (Maybe a) 
        ...plus 7 others
        (use -fprint-potential-instances to see them all)
    • In the expression: mempty'
      In the Template Haskell splice $$(mempty')
      In the first argument of ‘id’, namely ‘$$(mempty')’

Anotasi mempty' dapat menyelesaikannya dalam kasus ini:

>>> x :: String
... x = id $$(mempty' :: Q (TExp String))

>>> x
""

Ada tiket terbuka yang menjelaskan masalah tersebut, tetapi jika Anda mengalami beberapa kesalahan aneh, ada baiknya untuk mengingatnya.

Bacaan lebih lanjut

Dalam posting ini, kami memperluas pengetahuan Template Haskell kami dengan TExp. Kami membuat contoh singkat di mana kami menghasilkan beberapa nilai selama waktu kompilasi yang nantinya dapat dilihat saat runtime. Untuk sumber daya lainnya tentang Template Haskell yang diketik, lihat tautan berikut:

Untuk tutorial Haskell lainnya, Anda dapat melihat artikel Haskell kami atau ikuti kami di Indonesia atau Sedang.