Rank2Types'in amacı nedir?


110

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?


Temelde HM tipi sistemden polimorfik lambda hesabına bir yükseltmedir. λ2 / Sistem F. Bu tür çıkarımının λ2'de karar verilemez olduğunu unutmayın.
Poscat

Yanıtlar:


116

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ü gtanı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 mapkullanı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.idString -> Stringid


5
Cevabımı buna bağlayan bazı kelimeler eklemek için: Haskell işlevini düşünün 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 aiş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 -> rve şimdi gargümanı düzelttik, böylece alabilir Intama 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?
Luis Casillas

166

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 -> bHaskell'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 = Λtx: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 tve 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 idbir 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:

tx: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. Rank2Typesve RankNTypesbu 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 rve aardından ortaya çıkan türden bir bağımsız değişken sağlaması gerekir. Yani almak olabilir r = Intve 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 ∀aise içeride ne tip için kullanımına seçemiyorum böylece, işlev ok a; f' Intuygun 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ğiaf' 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 Intdiğeri a döndüren bir nesne şu türle Stringuygulanabilir:

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 → Stringbilinmeyen için olan nesnenin ikinci yöntemini çağırıyoruz a. myObjectMüşterileri tarafından bilinmeyen ; ancak bu istemciler imzadan, iki işlevden birini ona uygulayabileceklerini ve bir Intveya 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 , Showsı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 ExistentialTypesve 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.


2
Çok ayrıntılı bir cevap için teşekkürler! (bu, tesadüfen, sonunda beni uygun tip teorisini ve Sistem F'yi öğrenmeye motive etti)
Aleksandar Dimitrov

5
existsAnahtar 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, Anybunun da bir türe sahip olabileceği forall a. a -> Anyve forallanahtar 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).
Vitus

2
@Vitus: GHC varoluş bilgileri tip sınıf sözlüklerine bağlı değildir. Sahip olabilirsiniz data ApplyBox r = forall a. ApplyBox (a -> r) a; ne zaman sen desen maç ApplyBox f x, sen almak f :: h -> rve x :: hbir "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 Casillas

Bu, üzerinde biraz zaman harcamak zorunda kalacağım harika bir cevap. C # jeneriklerinin sağladığı soyutlamalara çok alışkın olduğumu düşünüyorum, bu yüzden teoriyi gerçekten anlamak yerine bunların çoğunu hafife alıyordum.
Andrey Shchekin

@sacundim: "Gerekli tüm daktilo sözlükleri", ihtiyacınız yoksa hiç sözlük olmadığı anlamına da gelebilir. :) Demek istediğim, GHC büyük olasılıkla varoluşsal türleri daha yüksek dereceli türler aracılığıyla kodlamıyor (yani önerdiğiniz dönüşüm - ∃xP (x) ~ ∀r. (∀xP (x) → r) → r).
Vitus

47

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 IOve o hedefe füze fırlatan bir eylemi döndürmesi gereken bir işlevi üstlenir . fBasit 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 f1 polimorfik tip verilmesi

f :: ([a] -> a) -> IO ()

Biz türünü seçin, çünkü hiç değil bu moral adediğimiz zaman f, ve biz sadece bunu uzmanlaşmak Countryve bizim kötü niyetli kullanmak \_ -> BestAllyyine. Çö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 \_ -> BestAllycheck 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, STmonad'ı güvenli hale getirmenin anahtarıdır .


18

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ı Tgerektiğ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. IEmployeeVisitorDönüş türünü evrensel olarak ölçtüğünü, diğer yandan IEmployeeda Acceptyö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.


1
C # / Java / Blub'ın üst düzey türler için desteği hakkında başka birinin yazıp yazmadığını bilmek isterim. Sevgili okuyucu, bu tür kaynaklardan haberiniz varsa, lütfen onlara benim yolumu gönderin!
Benjamin Hodgson


-2

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 Identifytalep ettiğini görün Identifier? Bu, Identifydaha yüksek dereceli bir işlev yapar .


Bu sepp2k'nin cevabına ne ekler?
dfeuer

Veya Benjamin Hodgson's, bu konuda?
dfeuer

1
Hodgson'ın fikrini kaçırdığını düşünüyorum. Acceptrank-1 polimorfik türü vardır, ancak IEmployeekendisi rank-2 olan bir yöntemdir . Biri bana bir verirse IEmployee, onu açabilir ve Acceptyöntemini her türden kullanabilirim.
dfeuer

1
Örneğiniz, Visiteetanıttığınız sınıf yoluyla da 2. derece . f :: Visitee e => T eTemelde 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.
dfeuer

1
Benim örneğimde öne çıkamazsınız 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.
dfeuer
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.