Dapatkah saya mencetak di Haskell jenis fungsi polimorfik seperti yang akan terjadi jika saya meneruskannya ke entitas jenis konkret?
Berikut adalah fungsi polimorfik dalam 3 jenis:
:t (.)
(.) :: (b -> c) -> (a -> b) -> a -> c
dan di sini fungsi non polimorfik:
:t Data.Char.digitToInt
Data.Char.digitToInt :: Char -> Int
Jika kita menerapkan yang pertama ke yang terakhir, kita mendapatkan fungsi polimorfik dalam 1 tipe:
:t (.) Data.Char.digitToInt
(.) Data.Char.digitToInt :: (a -> Char) -> a -> Int
yang artinya (.)
itu "dipakai" (saya tidak yakin ini istilah yang benar; sebagai programmer C ++, saya akan menyebutnya begitu) dengan b === Char
dan c === Int
, jadi tanda tangan dari (.)
yang diterapkan digitToInt
adalah sebagai berikut
(Char -> Int) -> (a -> Char) -> a -> Int
Pertanyaan saya adalah: adakah cara agar tanda tangan ini dicetak di layar, diberikan (.)
, digitToInt
dan "informasi" yang ingin saya terapkan yang pertama ke yang terakhir?
Bagi yang tertarik, pertanyaan ini sebelumnya ditutup sebagai duplikat dari pertanyaan ini .
Jawaban
Jawaban lain memerlukan bantuan fungsi yang telah ditentukan dengan tipe yang dibatasi secara artifisial, seperti asTypeOf
fungsi dalam jawaban dari HTNW. Ini tidak perlu, karena interaksi berikut menunjukkan:
Prelude> let asAppliedTo f x = const f (f x)
Prelude> :t head `asAppliedTo` "x"
head `asAppliedTo` "x" :: [Char] -> Char
Prelude> :t (.) `asAppliedTo` Data.Char.digitToInt
(.) `asAppliedTo` Data.Char.digitToInt
:: (Char -> Int) -> (a -> Char) -> a -> Int
Ini mengeksploitasi kurangnya polimorfisme dalam pengikatan lambda yang tersirat dalam definisi asAppliedTo
. Kedua kemunculan f
dalam tubuhnya harus diberi jenis yang sama, dan itulah jenis akibatnya. Fungsi yang const
digunakan di sini juga memiliki tipe alaminya a -> b -> a
:
const x y = x
Ada fungsi kecil rapi yang tersembunyi di sudut Prelude
:
Prelude.asTypeOf :: a -> a -> a
asTypeOf x _ = x
Ini didokumentasikan sebagai "memaksa argumen pertamanya memiliki tipe yang sama dengan yang kedua." Kita bisa menggunakan ini untuk memaksa tipe dari (.)
argumen pertama:
-- (.) = \x -> (.) x = \x -> (.) $ x `asTypeOf` Data.Char.digitToInt -- eta expansion followed by definition of asTypeOf -- the RHS is just (.), but restricted to arguments with the same type as digitToInt -- "what is the type of (.) when the first argument is (of the same type as) digitToInt?" ghci> :t \x -> (.) $ x `asTypeOf` Data.Char.digitToInt
\x -> (.) $ x `asTypeOf` Data.Char.digitToInt
:: (Char -> Int) -> (a -> Char) -> a -> Int
Tentu saja, ini berfungsi untuk argumen sebanyak yang Anda butuhkan.
ghci> :t \x y -> (x `asTypeOf` Data.Char.digitToInt) . (y `asTypeOf` head)
\x y -> (x `asTypeOf` Data.Char.digitToInt) . (y `asTypeOf` head)
:: (Char -> Int) -> ([Char] -> Char) -> [Char] -> Int
Anda dapat menganggap ini sebagai variasi dari ide @ KABuhr di komentar — menggunakan fungsi dengan tanda tangan yang lebih membatasi daripada implementasinya untuk memandu jenis inferensi — kecuali kita tidak harus mendefinisikan apa pun sendiri, dengan konsekuensi karena tidak bisa hanya salin ekspresi yang dimaksud di bawah lambda.
Saya pikir jawaban @ HTNW mungkin mencakupnya, tetapi untuk kelengkapan, berikut cara kerja inContext
solusinya secara detail.
Jenis tanda tangan dari fungsi tersebut:
inContext :: a -> (a -> b) -> a
artinya, jika Anda memiliki sesuatu yang ingin Anda ketik, dan "konteks" yang digunakan (diekspresikan sebagai lambda yang menganggapnya sebagai argumen), katakan dengan tipe:
thing :: a1
context :: a2 -> b
Anda dapat memaksa penyatuan a1
(tipe umum thing
) dengan a2
(batasan konteks) hanya dengan membuat ekspresi:
thing `inContext` context
Biasanya, tipe terpadu thing :: a
akan hilang, tetapi tipe tanda tangan inContext
menyiratkan bahwa tipe dari keseluruhan ekspresi yang dihasilkan ini juga akan disatukan dengan tipe yang diinginkan a
, dan GHCi akan dengan senang hati memberi tahu Anda tipe ekspresi itu.
Jadi ungkapannya:
(.) `inContext` \hole -> hole digitToInt
akhirnya mendapatkan jenis yang (.)
akan memiliki dalam konteks yang ditentukan. Anda dapat menulis ini, agak menyesatkan, sebagai:
(.) `inContext` \(.) -> (.) digitToInt
karena (.)
sama bagusnya dengan nama argumen untuk lambda anonim hole
. Ini berpotensi membingungkan, karena kita membuat pengikatan lokal yang membayangi definisi tingkat atas (.)
, tetapi masih menamai hal yang sama (dengan tipe yang disempurnakan), dan penyalahgunaan lambda ini memungkinkan kita untuk menulis ekspresi asli (.) digitToInt
secara verbatim, dengan boilerplate yang sesuai.
Sebenarnya tidak relevan bagaimana inContext
didefinisikan, jika Anda hanya meminta GHCi untuk tipenya, itu inContext = undefined
akan berhasil. Tapi, hanya dengan melihat tipe tanda tangan, cukup mudah untuk memberikan inContext
definisi yang berfungsi:
inContext :: a -> (a -> b) -> a
inContext a _ = a
Ternyata ini hanya definisi const
, jadi inContext = const
berfungsi juga.
Anda dapat menggunakan inContext
untuk mengetik beberapa hal sekaligus, dan itu bisa berupa ekspresi, bukan nama. Untuk mengakomodasi yang pertama, Anda dapat menggunakan tupel; agar yang terakhir berfungsi, Anda harus menggunakan nama argumen yang lebih masuk akal di lambas Anda.
Misalnya:
λ> :t (fromJust, fmap length) `inContext` \(a,b) -> a . b
(fromJust, fmap length) `inContext` \(a,b) -> a . b
:: Foldable t => (Maybe Int -> Int, Maybe (t a) -> Maybe Int)
memberi tahu Anda bahwa dalam ekspresi fromJust . fmap length
, tipe telah dikhususkan untuk:
fromJust :: Maybe Int -> Int
fmap length :: Foldable t => Maybe (t a) -> Maybe Int
Anda dapat melakukannya menggunakan TypeApplications
ekstensi, yang memungkinkan Anda untuk secara eksplisit menentukan tipe yang ingin Anda gunakan untuk membuat contoh parameter tipe:
λ :set -XTypeApplications
λ :t (.) @Char @Int
(.) @Char @Int :: (Char -> Int) -> (a -> Char) -> a -> Int
Perhatikan bahwa argumen harus dalam urutan yang tepat.
Untuk fungsi yang memiliki jenis tanda tangan "biasa" foo :: a -> b
, urutan ditentukan oleh urutan parameter jenis pertama kali muncul dalam tanda tangan.
Untuk fungsi yang menggunakan ExplicitForall
like foo :: forall b a. a -> b
, urutannya ditentukan oleh apa pun isinya forall
.
Jika Anda ingin mengetahui jenis khusus berdasarkan menerapkan (.)
ke digitToChar
(sebagai lawan hanya mengetahui yang jenis untuk mengisi), aku cukup yakin Anda tidak bisa di GHCi, tapi aku bisa sangat merekomendasikan dukungan Haskell IDE.
Misalnya, berikut ini tampilan saya di VSCode (inilah ekstensinya ):

Ini adalah variasi kecil dari jawaban HTNW.
Misalkan kita memiliki ekspresi yang berpotensi besar yang melibatkan pengenal polimorfik poly
.... poly ....
dan kami bertanya-tanya bagaimana tipe polimorfik dipakai pada saat itu.
Ini dapat dilakukan dengan memanfaatkan dua fitur GHC: asTypeOf
(seperti yang disebutkan oleh HTNW) dan lubang yang diketik , sebagai berikut:
.... (poly `asTypeOf` _) ....
Setelah membaca _
lubang, GHC akan menghasilkan kesalahan yang melaporkan jenis istilah yang harus dimasukkan untuk menggantikan lubang itu. Sejak kami menggunakan asTypeOf
, ini harus sama dengan jenis contoh khusus yang poly
kami butuhkan dalam konteks itu.
Berikut contoh di GHCi:
> ((.) `asTypeOf` _) Data.Char.digitToInt
<interactive>:11:17: error:
* Found hole: _ :: (Char -> Int) -> (a -> Char) -> a -> Int