Functor
Haskell'deki tür sınıfını düşünün , burada f
daha yüksek türde bir tür değişkeni vardır:
class Functor f where
fmap :: (a -> b) -> f a -> f b
Bu tür imzasının söylediği şey, fmap'in bir türünün tür parametresini f
olarak a
değiştirdiği b
, ancak f
olduğu gibi bıraktığıdır . Yani fmap
bir liste üzerinden kullanırsanız bir liste alırsınız, eğer onu bir ayrıştırıcı üzerinde kullanırsanız bir ayrıştırıcı alırsınız ve bu böyle devam eder. Ve bunlar statik , derleme zamanı garantileridir.
F # bilmiyorum, ama Functor
soyutlamayı Java veya C # gibi bir dilde, kalıtım ve jenerikle ifade etmeye çalışırsak , ancak daha yüksek türden jenerikler olmadan ne olacağını düşünelim. İlk deneme:
interface Functor<A> {
Functor<B> map(Function<A, B> f);
}
Bu ilk denemedeki sorun, arabirimin gerçeklemesinin uygulayan herhangi bir sınıfı döndürmesine izin verilmesidir Functor
. Biri yazabileceGin FunnyList<A> implements Functor<A>
kimin map
yöntem hiç bir koleksiyon değil ama yine de bir olduğu koleksiyon, ya da başka hatta şeyin farklı bir tür döner Functor
. Ayrıca, map
yöntemi kullandığınızda, aslında beklediğiniz türe indirgemediğiniz sürece, sonuçta alt türe özgü herhangi bir yöntemi çağıramazsınız. Yani iki sorunumuz var:
- Tür sistemi,
map
yöntemin her zaman Functor
alıcıyla aynı alt sınıfı döndürdüğü değişmezi ifade etmemize izin vermez .
- Bu nedenle,
Functor
sonucunda yöntem olmayan bir yöntemi çağırmanın statik olarak güvenli bir yolu yoktur map
.
Deneyebileceğiniz başka, daha karmaşık yollar var, ancak hiçbiri gerçekten işe yaramıyor. Örneğin Functor
, sonuç türünü kısıtlayan alt türlerini tanımlayarak ilk denemeyi artırmayı deneyebilirsiniz :
interface Collection<A> extends Functor<A> {
Collection<B> map(Function<A, B> f);
}
interface List<A> extends Collection<A> {
List<B> map(Function<A, B> f);
}
interface Set<A> extends Collection<A> {
Set<B> map(Function<A, B> f);
}
interface Parser<A> extends Functor<A> {
Parser<B> map(Function<A, B> f);
}
Bu yanlış türde dönen gelenler dar arayüzleri uygulayıcıların yasaklamak için yardımcı olur Functor
den map
yöntemle, ama kaç sınırı yok çünkü Functor
sen olabilir uygulamaları, ihtiyacınız kaç dar arayüzleri konusunda herhangi bir sınırlama yoktur.
( DÜZENLEME: Ve bunun yalnızca Functor<B>
sonuç türü olarak göründüğü için işe yaradığını ve bu nedenle alt arabirimlerin onu daraltabileceğini unutmayın. Bu nedenle AFAIK Monad<B>
, aşağıdaki arabirimde her iki kullanımı da daraltamayız :
interface Monad<A> {
<B> Monad<B> flatMap(Function<? super A, ? extends Monad<? extends B>> f);
}
Haskell'de, daha yüksek dereceli tip değişkenlerle, bu (>>=) :: Monad m => m a -> (a -> m b) -> m b
.)
Yine başka bir deneme, ara yüzün alt tipin sonuç türünü alt tipin kendisiyle sınırlandırmasını sağlamak için özyinelemeli jenerikler kullanmaktır. Oyuncak örneği:
interface Semigroup<T extends Semigroup<T>> {
T append(T arg);
}
class Foo implements Semigroup<Foo> {
Foo append(Foo arg);
}
class Bar implements Semigroup<Bar> {
Semigroup<Bar> append(Semigroup<Bar> arg);
Semigroup<Foo> append(Bar arg);
Semigroup append(Bar arg);
Foo append(Bar arg);
}
Ancak bu tür bir teknik (bu, sıradan bir OOP geliştiriciniz için oldukça gizemli, aynı zamanda sıradan işlevsel geliştiricinize de bakın) hala istenen Functor
kısıtlamayı ifade edemez :
interface Functor<FA extends Functor<FA, A>, A> {
<FB extends Functor<FB, B>, B> FB map(Function<A, B> f);
}
Buradaki sorun bu engel olmayacak FB
aynı olması F
olarak FA
bir türü bildirmek zaman o-yani List<A> implements Functor<List<A>, A>
, map
yöntem olabilir hala bir dönüş NotAList<B> implements Functor<NotAList<B>, B>
.
Ham türleri (parametresiz kapsayıcılar) kullanarak Java'da son deneme:
interface FunctorStrategy<F> {
F map(Function f, F arg);
}
Burada F
, sadece List
veya gibi parametresiz tiplere örneklenecektir Map
. Bu FunctorStrategy<List>
, List
a'nın yalnızca a döndürebileceğini garanti eder - ancak listelerin öğe türlerini izlemek için tür değişkenlerini kullanmayı bıraktınız.
Buradaki sorunun özü, Java ve C # gibi dillerin tür parametrelerinin parametrelere sahip olmasına izin vermemesidir. Eğer Java'da, T
bir tür değişkendir, yazabilir T
ve List<T>
ancak, T<String>
. Daha yüksek türden türler bu kısıtlamayı kaldırır, böylece böyle bir şeye sahip olabilirsiniz (tam olarak düşünülmemiş):
interface Functor<F, A> {
<B> F<B> map(Function<A, B> f);
}
class List<A> implements Functor<List, A> {
<B> List<B> map(Function<A, B> f) {
}
}
Ve özellikle bu kısma değinmek:
(Sanırım) Bunu anlıyorum myList |> List.map f
veya myList |> Seq.map f |> Seq.toList
daha yüksek türden türler basitçe yazmanıza izin veriyor myList |> map f
ve bir List
. Bu harika (doğru olduğunu varsayarsak), ama biraz önemsiz görünüyor? (Ve basitçe fonksiyon aşırı yüklemesine izin vererek yapılamaz mıydı?) Genellikle Seq
yine de dönüştürürüm ve daha sonra istediğim şeye dönüştürebilirim.
map
İşlev fikrini , özünde haritalama dizilerle ilgiliymiş gibi modelleyerek bu şekilde genelleştiren birçok dil vardır . Bu yorumunuz bu ruhta: Eğer Seq
içeri ve dışarı dönüşümü destekleyen bir türünüz varsa , yeniden kullanarak harita işlemini "ücretsiz" alırsınız Seq.map
.
Haskell'de ise Functor
sınıf bundan daha geneldir; diziler kavramına bağlı değildir. Eylemler, ayrıştırıcı birleştiriciler, işlevler vb. fmap
Gibi dizilerle iyi bir eşlemesi olmayan türler için uygulayabilirsiniz IO
:
instance Functor IO where
fmap f action =
do x <- action
return (f x)
newtype Function a b = Function (a -> b)
instance Functor (Function a) where
fmap f (Function g) = Function (f . g)
"Haritalama" kavramı aslında dizilere bağlı değildir. Functor yasalarını anlamak en iyisidir:
(1) fmap id xs == xs
(2) fmap f (fmap g xs) = fmap (f . g) xs
Gayri resmi olarak:
- Birinci yasa, bir kimlik / noop işleviyle haritalamanın hiçbir şey yapmamakla aynı olduğunu söylüyor.
- İkinci yasa, iki kez eşleştirerek üretebileceğiniz herhangi bir sonucu, bir kez eşleyerek de üretebileceğinizi söylüyor.
Bu yüzden fmap
türü korumak istiyorsunuz - çünkü map
farklı bir sonuç türü üreten operasyonlar alır almaz, bunun gibi garantiler vermek çok çok daha zor hale geliyor.