Czy mogę wydrukować w Haskell taki typ funkcji polimorficznej, jaką stałoby się, gdybym przekazał do niej jednostkę konkretnego typu?
Oto funkcja polimorficzna w 3 typach:
:t (.)
(.) :: (b -> c) -> (a -> b) -> a -> c
a tutaj funkcja niepolimorficzna:
:t Data.Char.digitToInt
Data.Char.digitToInt :: Char -> Int
Jeśli zastosujemy to pierwsze do drugiego, otrzymamy funkcję polimorficzną w 1 typie:
:t (.) Data.Char.digitToInt
(.) Data.Char.digitToInt :: (a -> Char) -> a -> Int
co oznacza, że (.)
został „utworzony” (nie jestem pewien, czy to jest poprawny termin; jako programista C ++ tak to nazwałbym) z b === Char
i c === Int
, więc podpis tego, do (.)
którego zostanie zastosowany, digitToInt
jest następujący
(Char -> Int) -> (a -> Char) -> a -> Int
Moje pytanie brzmi: czy jest jakiś sposób, aby ten podpis drukowany na ekranie, biorąc pod uwagę (.)
, digitToInt
a „informacje”, że chcę zastosować były do tej ostatniej?
Dla zainteresowanych to pytanie zostało wcześniej zamknięte jako duplikat tego .
Odpowiedzi
Inne odpowiedzi wymagają pomocy funkcji, które zostały zdefiniowane za pomocą sztucznie ograniczonych typów, takich jak asTypeOf
funkcja w odpowiedzi z HTNW. Nie jest to konieczne, jak pokazuje następująca interakcja:
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
Wykorzystuje to brak polimorfizmu w wiązaniu lambda, który jest domniemany w definicji asAppliedTo
. Oba wystąpienia f
w jego ciele muszą mieć ten sam typ i taki jest typ wyniku. Zastosowana const
tutaj funkcja również ma swój naturalny typ a -> b -> a
:
const x y = x
Jest taka zgrabna mała funkcja ukryta w rogu Prelude
:
Prelude.asTypeOf :: a -> a -> a
asTypeOf x _ = x
Jest to udokumentowane jako „wymuszanie tego samego typu pierwszego argumentu, co drugi”. Możemy użyć tego do wymuszenia typu (.)
pierwszego argumentu:
-- (.) = \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
Oczywiście działa to w przypadku tylu argumentów, ile potrzebujesz.
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
Możesz uznać to za odmianę pomysłu @ KABuhr w komentarzach - używając funkcji z podpisem bardziej restrykcyjnym niż jej implementacja, aby kierować wnioskami o typie - z wyjątkiem tego, że nie musimy niczego definiować sami, kosztem niemożności po prostu skopiuj dane wyrażenie pod lambdą.
Myślę, że odpowiedź @ HTNW prawdopodobnie to obejmuje, ale dla kompletności, oto jak inContext
szczegółowo działa to rozwiązanie.
Podpis typu funkcji:
inContext :: a -> (a -> b) -> a
oznacza, że jeśli masz coś, co chcesz wpisać, i „kontekst”, w którym jest używany (wyrażany jako lambda przyjmująca ją jako argument), powiedz z typami:
thing :: a1
context :: a2 -> b
Możesz wymusić ujednolicenie a1
(ogólnego typu thing
) z a2
(ograniczeniami kontekstu), po prostu konstruując wyrażenie:
thing `inContext` context
Zwykle ujednolicony typ thing :: a
zostałby utracony, ale sygnatura typu inContext
oznacza, że typ całego tego wyrażenia wynikowego będzie również ujednolicony z żądanym typem a
, a GHCi z radością poinformuje Cię o typie tego wyrażenia.
A więc wyrażenie:
(.) `inContext` \hole -> hole digitToInt
kończy się przypisaniem typu, który (.)
miałby w określonym kontekście. Możesz to napisać nieco myląco, jako:
(.) `inContext` \(.) -> (.) digitToInt
ponieważ (.)
jest równie dobrą nazwą argumentu dla anonimowej lambdy, jak hole
jest. Jest to potencjalnie mylące, ponieważ tworzymy lokalne powiązanie, które przesłania definicję najwyższego poziomu (.)
, ale nadal nazywa to samo (z wyrafinowanym typem), a to nadużycie lambd pozwoliło nam napisać oryginalne wyrażenie (.) digitToInt
dosłownie, z odpowiednim wzorcem.
Właściwie nie ma znaczenia, w jaki sposób inContext
jest zdefiniowany, jeśli pytasz tylko GHCi o jego typ, więc inContext = undefined
zadziałałoby. Ale patrząc na podpis typu, łatwo jest podać inContext
roboczą definicję:
inContext :: a -> (a -> b) -> a
inContext a _ = a
Okazuje się, że to tylko definicja const
, więc też inContext = const
działa.
Możesz użyć inContext
do wpisania wielu rzeczy naraz i mogą to być wyrażenia zamiast nazw. Aby dostosować się do tego pierwszego, możesz użyć krotek; aby ta ostatnia działała, musisz użyć bardziej rozsądnych nazw argumentów w swoich lambdach.
Na przykład:
λ> :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)
informuje, że w wyrażeniu fromJust . fmap length
typy zostały wyspecjalizowane do:
fromJust :: Maybe Int -> Int
fmap length :: Foldable t => Maybe (t a) -> Maybe Int
Możesz to zrobić za pomocą TypeApplications
rozszerzenia, które pozwala jawnie określić, których typów chcesz użyć do utworzenia wystąpienia parametrów typu:
λ :set -XTypeApplications
λ :t (.) @Char @Int
(.) @Char @Int :: (Char -> Int) -> (a -> Char) -> a -> Int
Zwróć uwagę, że argumenty muszą mieć dokładną kolejność.
W przypadku funkcji, które mają podpis typu „zwykłego”, np foo :: a -> b
. Kolejność jest definiowana przez kolejność, w jakiej parametry typu pojawiają się po raz pierwszy w podpisie.
W przypadku funkcji, które używają ExplicitForall
like foo :: forall b a. a -> b
, kolejność jest definiowana przez to, w czym się znajduje forall
.
Jeśli chcesz dowiedzieć się, jaki typ jest konkretnie w oparciu o zastosowanie (.)
do digitToChar
(w przeciwieństwie do zwykłej wiedzy, które typy wypełnić), jestem prawie pewien, że nie możesz tego zrobić w GHCi, ale mogę bardzo polecić obsługę Haskell IDE.
Na przykład, tak to wygląda dla mnie w VSCode (tutaj jest rozszerzenie ):

Jest to niewielka odmiana odpowiedzi HTNW.
Załóżmy, że mamy dowolne, potencjalnie duże, wyrażenie zawierające identyfikator polimorficzny poly
.... poly ....
i zastanawiamy się, jak w tym momencie utworzono wystąpienie typu polimorficznego.
Można to zrobić wykorzystując dwie cechy GHC: asTypeOf
(o czym wspomina HTNW) i wpisane dziury w następujący sposób:
.... (poly `asTypeOf` _) ....
Po odczytaniu _
otworu GHC wygeneruje błąd informujący o rodzaju terminu, który należy wprowadzić w miejsce tego otworu. Ponieważ używaliśmy asTypeOf
, musi to być to samo, co typ konkretnego wystąpienia, poly
którego potrzebujemy w tym kontekście.
Oto przykład w GHCi:
> ((.) `asTypeOf` _) Data.Char.digitToInt
<interactive>:11:17: error:
* Found hole: _ :: (Char -> Int) -> (a -> Char) -> a -> Int