Haskell konusunda gerçekten yetkin değilim, bu yüzden bu çok kolay bir soru olabilir.
Rank2Types hangi dil sınırlamasını çözer? Haskell'deki işlevler polimorfik argümanları zaten desteklemiyor mu?
Haskell konusunda gerçekten yetkin değilim, bu yüzden bu çok kolay bir soru olabilir.
Rank2Types hangi dil sınırlamasını çözer? Haskell'deki işlevler polimorfik argümanları zaten desteklemiyor mu?
Yanıtlar:
Haskell'deki işlevler polimorfik argümanları zaten desteklemiyor mu?
Yaparlar, ancak yalnızca 1. derece. Bu, bu uzantı olmadan farklı türde argümanlar alan bir işlev yazabileceğiniz anlamına gelirken, bağımsız değişkenini aynı çağrıda farklı türler olarak kullanan bir işlev yazamazsınız.
Örneğin, aşağıdaki işlev bu uzantı olmadan yazılamaz çünkü g
tanımında farklı bağımsız değişken türleriyle kullanılır f
:
f g = g 1 + g "lala"
Bir polimorfik işlevi başka bir işleve argüman olarak iletmenin tamamen mümkün olduğuna dikkat edin. Yani böyle bir şey map id ["a","b","c"]
tamamen yasaldır. Ancak işlev, onu yalnızca monomorfik olarak kullanabilir. Örnekte tipi varmış gibi map
kullanır . Ve tabii ki bunun yerine verilen tipte basit bir monomorfik fonksiyon da geçebilirsiniz . Rank2types olmadan, bir fonksiyonun argümanının polimorfik bir fonksiyon olması gerektiğini ve dolayısıyla onu bir polimorfik fonksiyon olarak kullanmanın bir yolu yoktur.id
String -> String
id
f' g x y = g x + g y
. Çıkarılan rank-1 türü forall a r. Num r => (a -> r) -> a -> a -> r
. Yana forall a
işlev oklar dışında, arayan ilk için bir tür almak gerekir a
; seçerlerse Int
, anlarız f' :: forall r. Num r => (Int -> r) -> Int -> Int -> r
ve şimdi g
argümanı düzelttik, böylece alabilir Int
ama değil String
. Eğer etkinleştirirsek RankNTypes
, yazı f'
ile açıklama ekleyebiliriz forall b c r. Num r => (forall a. a -> r) -> b -> c -> r
. Yine de kullanamazsın - ne olurdu g
?
Doğrudan Sistem F üzerinde çalışmadığınız sürece daha yüksek dereceli polimorfizmi anlamak zordur , çünkü Haskell basitlik adına bunun ayrıntılarını sizden gizlemek için tasarlanmıştır.
Ancak temelde, kaba fikir, polimorfik türlerin a -> b
Haskell'de sahip oldukları forma sahip olmadıklarıdır; gerçekte, her zaman açık nicelik belirteçleriyle şöyle görünürler:
id :: ∀a.a → a
id = Λt.λx:t.x
"∀" simgesini bilmiyorsanız, "herkes için" olarak okunur; ∀x.dog(x)
"tüm x için, x bir köpektir" anlamına gelir. "Λ" büyük lambda'dır ve tür parametrelerinin soyutlanması için kullanılır; ikinci satırın söylediği, id'nin bir türü alan t
ve ardından bu türe göre parametreleştirilmiş bir işlev döndüren bir işlev olduğudur.
Görüyorsunuz, Sistem F'de, böyle bir işlevi id
bir değere hemen uygulayamazsınız ; Bir değere uygulayacağınız bir λ işlevi elde etmek için önce function işlevini bir türe uygulamanız gerekir. Yani mesela:
(Λt.λx:t.x) Int 5 = (λx:Int.x) 5
= 5
Standart Haskell (yani Haskell 98 ve 2010), bu tür niceleyicilerden, büyük lambdalardan ve tür uygulamalarından hiçbirine sahip olmayarak bunu sizin için basitleştirir, ancak sahne arkasına GHC programı derleme için analiz ederken koyar. (Bu derleme zamanıyla ilgili şeyler olduğuna inanıyorum, çalışma zamanı ek yükü yok.)
Ancak Haskell'in bunu otomatik olarak ele alması, bir fonksiyon ("→") türünün sol kolunda asla "∀" görünmediğini varsaydığı anlamına gelir. Rank2Types
ve RankNTypes
bu kısıtlamaları kapatın ve Haskell'in nereye ekleneceğine ilişkin varsayılan kurallarını geçersiz kılmanıza izin verin forall
.
Bunu neden yapmak istersiniz? Çünkü tam, sınırsız System F çok güçlüdür ve pek çok harika şey yapabilir. Örneğin, tür gizleme ve modülerlik, daha yüksek dereceli türler kullanılarak uygulanabilir. Örneğin, aşağıdaki 1. derece türündeki düz eski bir işlevi ele alın (sahneyi ayarlamak için):
f :: ∀r.∀a.((a → r) → a → r) → r
Kullanmak için f
, arayanın önce hangi türler için kullanılacağını seçmesi r
ve a
ardından ortaya çıkan türden bir bağımsız değişken sağlaması gerekir. Yani almak olabilir r = Int
ve a = String
:
f Int String :: ((String → Int) → String → Int) → Int
Ama şimdi bunu aşağıdaki daha yüksek dereceli türle karşılaştırın:
f' :: ∀r.(∀a.(a → r) → a → r) → r
Bu tür bir işlev nasıl çalışır? Peki, onu kullanmak için önce hangi türün kullanılacağını belirlersiniz r
. Diyelim ki seçelim Int
:
f' Int :: (∀a.(a → Int) → a → Int) → Int
Ama şimdi ∀a
ise içeride ne tip için kullanımına seçemiyorum böylece, işlev ok a
; f' Int
uygun türde bir Λ işlevine başvurmanız gerekir . Bu , uygulamasının , arayanın değil, f'
hangi tür için kullanılacağını seçeceğia
f'
anlamına gelir . Daha yüksek dereceli türler olmadan, aksine, arayan her zaman türleri seçer.
Bu ne işe yarar? Aslında pek çok şey için, ama bir fikir, "nesnelerin" gizli veriler üzerinde çalışan bazı yöntemlerle birlikte bazı gizli verileri bir araya getirdiği nesne yönelimli programlama gibi şeyleri modellemek için bunu kullanabileceğinizdir. Örneğin, iki yöntemi olan bir nesne - biri an döndüren Int
diğeri a döndüren bir nesne şu türle String
uygulanabilir:
myObject :: ∀r.(∀a.(a → Int, a -> String) → a → r) → r
Bu nasıl çalışıyor? Nesne, gizli tipte bazı dahili verilere sahip bir işlev olarak uygulanır a
. Nesneyi gerçekten kullanmak için, istemcileri, nesnenin iki yöntemle çağıracağı bir "geri arama" işlevini iletir. Örneğin:
myObject String (Λa. λ(length, name):(a → Int, a → String). λobjData:a. name objData)
Burada, temel olarak, türü a → String
bilinmeyen için olan nesnenin ikinci yöntemini çağırıyoruz a
. myObject
Müşterileri tarafından bilinmeyen ; ancak bu istemciler imzadan, iki işlevden birini ona uygulayabileceklerini ve bir Int
veya a alabileceklerini biliyorlar String
.
Gerçek bir Haskell örneği için, kendi kendime öğretirken yazdığım kod aşağıdadır RankNTypes
. Bu, adı verilen bir türü uygularShowBox
, Show
sınıf örneğiyle birlikte bazı gizli türlerin bir değerini paketleyen . En alttaki örnekte ShowBox
, birinci elemanı bir sayıdan ve ikincisi bir dizeden yapılmış olanların bir listesini yaptığıma dikkat edin. Türler, daha yüksek sıralı türler kullanılarak gizlendiğinden, bu tür denetimini ihlal etmez.
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ImpredicativeTypes #-}
type ShowBox = forall b. (forall a. Show a => a -> b) -> b
mkShowBox :: Show a => a -> ShowBox
mkShowBox x = \k -> k x
-- | This is the key function for using a 'ShowBox'. You pass in
-- a function @k@ that will be applied to the contents of the
-- ShowBox. But you don't pick the type of @k@'s argument--the
-- ShowBox does. However, it's restricted to picking a type that
-- implements @Show@, so you know that whatever type it picks, you
-- can use the 'show' function.
runShowBox :: forall b. (forall a. Show a => a -> b) -> ShowBox -> b
-- Expanded type:
--
-- runShowBox
-- :: forall b. (forall a. Show a => a -> b)
-- -> (forall b. (forall a. Show a => a -> b) -> b)
-- -> b
--
runShowBox k box = box k
example :: [ShowBox]
-- example :: [ShowBox] expands to this:
--
-- example :: [forall b. (forall a. Show a => a -> b) -> b]
--
-- Without the annotation the compiler infers the following, which
-- breaks in the definition of 'result' below:
--
-- example :: forall b. [(forall a. Show a => a -> b) -> b]
--
example = [mkShowBox 5, mkShowBox "foo"]
result :: [String]
result = map (runShowBox show) example
Not: Bunu okuyan ExistentialTypes
ve GHC'nin nasıl kullanıldığını merak eden herkes için forall
, bunun nedeninin perde arkasında bu tür bir tekniği kullanması olduğuna inanıyorum.
exists
Anahtar kelimeniz varsa , varoluşsal bir türü (örneğin) data Any = Any (exists a. a)
, nerede olarak tanımlayabilirsiniz Any :: (exists a. a) -> Any
. ∀xP (x) → Q ≡ (∃xP (x)) → Q kullanarak, Any
bunun da bir türe sahip olabileceği forall a. a -> Any
ve forall
anahtar kelimenin geldiği yer olduğu sonucuna varabiliriz . Varoluşsal türlerin GHC tarafından uygulandığı şekliyle, gerekli tüm tip sınıfı sözlüklerini de taşıyan sıradan veri türleri olduğuna inanıyorum (bunu destekleyecek bir referans bulamadım, üzgünüm).
data ApplyBox r = forall a. ApplyBox (a -> r) a
; ne zaman sen desen maç ApplyBox f x
, sen almak f :: h -> r
ve x :: h
bir "gizli" Kısıtlı türü için h
. Doğru anlıyorsam, typeclass sözlük durumu şöyle bir şeye data ShowBox = forall a. Show a => ShowBox a
çevrilir : gibi bir şeye çevrilir data ShowBox' = forall a. ShowBox' (ShowDict' a) a
; instance Show ShowBox' where show (ShowBox' dict val) = show' dict val
; show' :: ShowDict a -> a -> String
.
Luis Casillo'nun cevabı 2. derece türlerin ne anlama geldiğiyle ilgili pek çok harika bilgi veriyor, ancak ben sadece onun kapsamadığı bir noktayı genişleteceğim. Bir argümanın polimorfik olmasını zorunlu kılmak, sadece onun birden çok türle kullanılmasına izin vermez; ayrıca bu işlevin bağımsız değişken (ler) i ile neler yapabileceğini ve sonucunu nasıl üretebileceğini de sınırlar. Yani arayan kişiye daha az verir esneklik sağlar. Bunu neden yapmak istersiniz? Basit bir örnekle başlayacağım:
Bir veri türünüz olduğunu varsayalım
data Country = BigEnemy | MediumEnemy | PunyEnemy | TradePartner | Ally | BestAlly
ve bir fonksiyon yazmak istiyoruz
f g = launchMissilesAt $ g [BigEnemy, MediumEnemy, PunyEnemy]
Verilen listenin öğelerinden birini seçmesi IO
ve o hedefe füze fırlatan bir eylemi döndürmesi gereken bir işlevi üstlenir . f
Basit bir tür verebiliriz :
f :: ([Country] -> Country) -> IO ()
Sorun, kazara kaçabilmemizdir.
f (\_ -> BestAlly)
ve sonra başımız büyük belaya girer! Seviye f
1 polimorfik tip verilmesi
f :: ([a] -> a) -> IO ()
Biz türünü seçin, çünkü hiç değil bu moral a
dediğimiz zaman f
, ve biz sadece bunu uzmanlaşmak Country
ve bizim kötü niyetli kullanmak \_ -> BestAlly
yine. Çözüm, 2. sıra türü kullanmaktır:
f :: (forall a . [a] -> a) -> IO ()
Şimdi ilettiğimiz işlevin polimorfik olması gerekiyor, bu yüzden \_ -> BestAlly
check yazmayacağız! Aslında, verilen listede olmayan bir öğeyi döndüren hiçbir işlev denetimi yazmayacaktır (sonsuz döngülere giren veya hata üreten ve bu nedenle asla geri dönen bazı işlevler bunu yapmayacaktır).
Yukarıdakiler elbette uydurulmuştur, ancak bu tekniğin bir varyasyonu, ST
monad'ı güvenli hale getirmenin anahtarıdır .
Daha yüksek dereceli türler, diğer yanıtların ortaya koyduğu kadar egzotik değildir. İster inanın ister inanmayın, birçok nesne yönelimli dil (Java ve C # dahil!) Bunlara sahiptir. (Elbette, bu topluluklardaki hiç kimse onları korkutucu bir ad olan "üst düzey tipler" ile tanımaz.)
Vereceğim örnek, günlük işlerimde her zaman kullandığım Ziyaretçi modelinin bir ders kitabı uygulamasıdır . Bu yanıt, ziyaretçi modeline bir giriş olarak tasarlanmamıştır; bu bilgi başka bir yerde kolayca elde edilebilir .
Bu saçma hayali İK uygulamasında, tam zamanlı kalıcı personel veya geçici yüklenici olabilecek çalışanlar üzerinde çalışmak istiyoruz. Ziyaretçi modelinin (ve aslında alakalı olanın) tercih ettiğim varyantı RankNTypes
, ziyaretçinin dönüş türünü parametreler.
interface IEmployeeVisitor<T>
{
T Visit(PermanentEmployee e);
T Visit(Contractor c);
}
class XmlVisitor : IEmployeeVisitor<string> { /* ... */ }
class PaymentCalculator : IEmployeeVisitor<int> { /* ... */ }
Mesele şu ki, farklı dönüş türlerine sahip bir dizi ziyaretçinin hepsi aynı veriler üzerinde çalışabilir. Bu IEmployee
, ne olması T
gerektiği konusunda hiçbir fikir belirtmemelidir .
interface IEmployee
{
T Accept<T>(IEmployeeVisitor<T> v);
}
class PermanentEmployee : IEmployee
{
// ...
public T Accept<T>(IEmployeeVisitor<T> v)
{
return v.Visit(this);
}
}
class Contractor : IEmployee
{
// ...
public T Accept<T>(IEmployeeVisitor<T> v)
{
return v.Visit(this);
}
}
Dikkatinizi türlere çekmek istiyorum. IEmployeeVisitor
Dönüş türünü evrensel olarak ölçtüğünü, diğer yandan IEmployee
da Accept
yöntemi içinde, yani daha yüksek bir sırada ölçtüğünü gözlemleyin . C # 'dan Haskell' e beceriksizce çeviri:
data IEmployeeVisitor r = IEmployeeVisitor {
visitPermanent :: PermanentEmployee -> r,
visitContractor :: Contractor -> r
}
newtype IEmployee = IEmployee {
accept :: forall r. IEmployeeVisitor r -> r
}
İşte orada var. Daha yüksek dereceli türler, genel yöntemler içeren türler yazdığınızda C # 'da görünür.
Bryan O'Sullivan'ın Stanford'daki Haskell kursundaki slaytlar anlamama yardımcı oldu Rank2Types
.
Nesne yönelimli dillere aşina olanlar için, daha yüksek seviyeli bir işlev, argüman olarak başka bir genel işlevi bekleyen basit bir genel işlevdir.
Örneğin TypeScript'te şunları yazabilirsiniz:
type WithId<T> = T & { id: number }
type Identifier = <T>(obj: T) => WithId<T>
type Identify = <TObj>(obj: TObj, f: Identifier) => WithId<TObj>
Genel işlev türünün, türün genel bir işlevini nasıl Identify
talep ettiğini görün Identifier
? Bu, Identify
daha yüksek dereceli bir işlev yapar .
Accept
rank-1 polimorfik türü vardır, ancak IEmployee
kendisi rank-2 olan bir yöntemdir . Biri bana bir verirse IEmployee
, onu açabilir ve Accept
yöntemini her türden kullanabilirim.
Visitee
tanıttığınız sınıf yoluyla da 2. derece . f :: Visitee e => T e
Temelde bir işlev (sınıf öğesinin şekeri kaldırıldığında) f :: (forall r. e -> Visitor e r -> r) -> T e
. Haskell 2010, bunun gibi sınıfları kullanarak sınırlı rank-2 polimorfizminden kurtulmanızı sağlar.
forall
. Elimde bir referans yok, ancak "Tip Sınıflarını Parçala" bölümünde bir şeyler bulabilirsin . Daha yüksek dereceli polimorfizm gerçekten de tip kontrol problemlerini ortaya çıkarabilir, ancak sınıf sisteminde örtük olan sınırlı sıralama iyidir.