C # 'da, bir monad nedir?


190

Bugünlerde monadlarla ilgili çok fazla konuşma var. Birkaç makale / blog yazısı okudum, ancak kavramını tam olarak kavramak için örnekleriyle yeterince ileri gidemiyorum. Bunun nedeni, monad'ların işlevsel bir dil kavramı olmasıdır ve bu nedenle örnekler, üzerinde çalışmadığım dillerdedir (derinlemesine fonksiyonel bir dil kullanmadığım için). Sözdizimi makaleleri tam olarak takip edecek kadar derinden kavrayamıyorum ... ama orada anlaşılmaya değer bir şey olduğunu söyleyebilirim.

Ancak, lambda ifadeleri ve diğer fonksiyonel özellikler de dahil olmak üzere C # 'ı oldukça iyi biliyorum. C # sadece fonksiyonel özelliklerin bir alt kümesi olduğunu biliyorum, ve bu yüzden belki monads C # ifade edilemez.

Ancak, kavramı iletmek elbette mümkün mü? En azından öyle umut ediyorum. Belki bir temel olarak bir C # örneği sunabilir ve daha sonra bir C # geliştiricisinin oradan ne yapmasını dilediğini açıklayabilirsin, ancak dil fonksiyonel programlama özelliklerinden yoksun. Bu harika olurdu, çünkü monadların niyetini ve faydalarını iletecekti. Yani benim sorum: Monads bir C # 3 geliştiricisine verebileceğiniz en iyi açıklama nedir?

Teşekkürler!

(DÜZENLEME: Bu arada, SO üzerinde zaten en az 3 "monad nedir" soruları olduğunu biliyorum. Ancak, onlarla aynı sorunla karşı karşıya ... Bu yüzden bu soru imo, çünkü C # geliştirici gerekli odaklanın.


Lütfen bunun aslında bir C # 3.0 geliştiricisi olduğunu unutmayın. .NET 3.5 ile karıştırmayın. Bunun dışında güzel bir soru.
Razzie

4
LINQ sorgu ifadelerinin C # 3'teki monadik davranışa bir örnek olduğunu belirtmek gerekir.
Erik Forbes

1
Ben hala bunun yinelenen bir soru olduğunu düşünüyorum. Cevapları biri stackoverflow.com/questions/2366/can-anyone-explain-monads bağlantı veren channel9vip.orcsweb.com/shows/Going+Deep/... yorumların biri çok güzel bir C # örnek vardır. :)
jalf

4
Yine de, bu, bir sorunun yanıtından SO sorularının birine sadece bir bağlantı. C # geliştiricileri odaklı bir soruda değer görüyorum. Biliyorsam, C # yapan fonksiyonel bir programcıya soracağım bir şeydi, bu yüzden SO'ya sormak makul görünüyor. Ama ben senin fikrin hakkına da saygı duyuyorum.
Charlie Flowers

1
Gereksinim duyduğunuz tek şey bir cevap değil mi? ;) Benim açımdan sadece diğer sorulardan biri (ve şimdi de bu, bu yüzden yay) C # -özel bir cevabı (ki gerçekten iyi yazılmış görünüyor, aslında. Muhtemelen gördüğüm en iyi açıklama)
jalf

Yanıtlar:


147

Tüm gün programlamada yaptığınız şeylerin çoğu, onlardan daha büyük fonksiyonlar oluşturmak için bazı fonksiyonları bir araya getirmektir. Genellikle sadece araç kutunuzda fonksiyonlara sahip olmanın yanı sıra operatörler, değişken atamaları ve benzerleri gibi başka şeyler de vardır, fakat genel olarak programınız daha fazla bir araya getirilecek daha büyük hesaplamalar için birçok "hesaplamayı" birleştirir.

Bir monad bunu "hesaplamaları birleştirmek" için yapmanın bir yoludur.

Genellikle iki hesaplamayı bir araya getirmek için en temel "operatörünüz" ;:

a; b

Bunu söylediğinizde "önce yapın a, sonra yapın " demek istersiniz b. Sonuç a; btemel olarak yine daha fazla şeyle birleştirilebilecek bir hesaplamadır. Bu basit bir monad, küçük hesaplamaları daha büyük olanlara taramanın bir yoludur. ;"O zaman sağda olanı yapmak, soldaki bir şey yapmaya" diyor.

Nesneye yönelik dillerde bir monad olarak görülebilen bir başka şey de .. Genellikle böyle şeyler bulursunuz:

a.b().c().d()

Temel .olarak "soldaki hesaplamayı değerlendirin ve ardından sağdaki yöntemi bunun sonucu olarak çağırın" anlamına gelir. Fonksiyonları / hesaplamaları bir araya getirmenin başka bir yoludur, biraz daha karmaşıktır ;. Ve şeyleri birlikte zincirleme kavramı .bir monaddır, çünkü iki hesaplamayı yeni bir hesaplamayla birleştirmenin bir yoludur.

Özel bir sözdizimi olmayan oldukça yaygın bir diğer monad, bu modeldir:

rv = socket.bind(address, port);
if (rv == -1)
  return -1;

rv = socket.connect(...);
if (rv == -1)
  return -1;

rv = socket.send(...);
if (rv == -1)
  return -1;

-1 döndürme değeri başarısızlığı gösterir, ancak bu şekilde birleştirmeniz gereken çok sayıda API çağrınız olsa bile bu hata denetimini soyutlamanın gerçek bir yolu yoktur. Bu sadece "soldaki işlev -1 döndürdüyse, -1 döndürürse, aksi takdirde sağdaki işlevi çağırır" kuralına göre işlev çağrılarını birleştiren başka bir monad. >>=Bu şeyi yapan bir operatörünüz olsaydı şunu yazabiliriz:

socket.bind(...) >>= socket.connect(...) >>= socket.send(...)

İşleri daha okunaklı hale getirecek ve fonksiyonlarımızı birleştirmenin özel yolunu ortaya çıkarmaya yardımcı olacak, böylece kendimizi tekrar tekrar tekrarlamamız gerekmiyor.

Ve genel bir örüntü olarak yararlı olan ve bir monad içinde soyutlanabilen işlevleri / hesaplamaları birleştirmenin daha birçok yolu vardır, böylece monad kullanıcısının daha açık ve net kod yazmasına olanak tanır, çünkü kullanılan fonksiyonlar monad'da yapılır.

Örneğin, yukarıdakiler >>="hata denetimini yapın ve giriş olarak aldığımız soketin sağ tarafını arayın" şeklinde genişletilebilir, böylece açıkça socketbirçok kez belirtmemiz gerekmez :

new socket() >>= bind(...) >>= connect(...) >>= send(...);

Biçimsel tanım biraz daha karmaşıktır, çünkü bir işlevin sonucunu bir sonrakine girdi olarak nasıl alacağınız konusunda endişelenmeniz gerekir, bu işlev bu girdiye ihtiyaç duyuyorsa ve birleştirdiğiniz işlevlerin uygun olduğundan emin olmak istiyorsanız onları monadınızda birleştirmeye çalışma şekliniz. Ancak temel kavram, fonksiyonları bir araya getirmenin farklı yollarını resmileştirmenizdir.


28
Mükemmel cevap! Monad'ları à la C ++ veya C # 'a aşırı yükleme operatörü ile ilişkilendirmeye çalışarak Oliver Steele tarafından bir alıntı yapacağım: Monads,'; ' Şebeke.
Jörg W Mittag

6
@ JörgWMittag Bu alıntıyı daha önce okudum, ama aşırı kafa gibi saçmalık gibiydi. Şimdi monadları anlıyorum ve nasıl ';' bir, anladım. Ama bence bu, çoğu zorunlu geliştirici için mantıksız bir ifade. ';' artık // en çok olan bir operatör olarak görülmüyor.
Jimmy Hoffa

2
Bir monadın ne olduğunu bildiğinden emin misin? Monadlar bir "işlev" veya hesaplama değildir, monadlar için kurallar vardır.
Luis

Senin içinde ;Örneğin: Ne / veri türlerini yok nesneleri ;harita? (Think Listharitalar Tiçin List<T>) yapar nasıl ;nesneleri / veri türleri arasında harita morfizimler / işlevleri? Ne pure, join, bindiçin ;?
Micha Wiedenmann

44

Bu soruyu yayınlamamdan bu yana bir yıl geçti. Gönderdikten sonra Haskell'i birkaç aylığına araştırdım. Çok keyif aldım, ama Monad'ları araştırmaya hazır olduğum için kenara koydum. İşe geri döndüm ve projemin gerektirdiği teknolojilere odaklandım.

Ve dün gece, bu cevapları tekrar okudum. En önemlisi , ben yeniden okumak özgü C # örnek metin yorumlarında Brian Beckman Video birisi bahseder yukarıda . Çok net ve aydınlatıcıydı, doğrudan buraya göndermeye karar verdim.

Bu yorum nedeniyle, sadece Monad'ların tam olarak ne olduğunu anladığımı hissetmekle kalmıyorum… C # 'da aslında Monad olan bazı şeyler yazdığımı fark ediyorum … ya da en azından çok yakın ve aynı sorunları çözmeye çalışıyorum.

Yani, burada açıklama var - bu olduğu tüm direkt bir alıntı burada açıklama ile sylvan :

Bu çok havalı. Yine de biraz soyut. Gerçek örneklerin yokluğu nedeniyle hangi monadların zaten karıştığını bilmeyen insanları hayal edebiliyorum.

Bu yüzden uymaya çalışmama izin verin ve gerçekten net olmak için, çirkin görünmesine rağmen C # 'da bir örnek yapacağım. Sonunda eşdeğer Haskell'i ekleyeceğim ve size IMO, monad'ların gerçekten işe yaramaya başladığı serin Haskell sözdizimsel şekeri göstereceğim.

Tamam, bu yüzden en kolay Monad'lardan birine Haskell'deki "Belki monad" denir. C # 'da Belki tipi çağrılır Nullable<T>. Temel olarak, geçerli ve bir değeri olan veya "boş" olan ve değeri olmayan bir değer kavramını içine alan küçük bir sınıftır.

Bu tipteki değerleri birleştirmek için bir monadın içine yapışmak için yararlı bir şey, başarısızlık kavramıdır. Yani, birden çok boş değerlere bakıp nullbunlardan herhangi biri boş olur olmaz geri dönmek istiyoruz . Örneğin, bir sözlükte veya başka bir şeyde çok sayıda anahtar ararsanız ve sonunda tüm sonuçları işlemek ve bunları bir şekilde birleştirmek istiyorsanız, ancak anahtarlardan herhangi biri sözlükte değilse, nullher şey için geri dönmek istiyorsun . Her aramayı manuel olarak kontrol etmek nullve geri dönmek zorunda kalmak sıkıcı olacaktır , bu nedenle bu kontrolü bağlama operatörünün içinde gizleyebiliriz (bu bir tür monadların noktasıdır, kod operatörünü daha kolay hale getiren bağlama operatörüne kitap tutmayı gizleriz ayrıntıları unutabileceğimiz için kullanın).

İşte her şeyi motive eden program ( Binddaha sonra tanımlayacağım , bu sadece neden güzel olduğunu göstermek için).

 class Program
    {
        static Nullable<int> f(){ return 4; }        
        static Nullable<int> g(){ return 7; }
        static Nullable<int> h(){ return 9; }


        static void Main(string[] args)
        {
            Nullable<int> z = 
                        f().Bind( fval => 
                            g().Bind( gval => 
                                h().Bind( hval =>
                                    new Nullable<int>( fval + gval + hval ))));

            Console.WriteLine(
                    "z = {0}", z.HasValue ? z.Value.ToString() : "null" );
            Console.WriteLine("Press any key to continue...");
            Console.ReadKey();
        }
    }

Şimdi, bir an için bunu zaten NullableC # için destek olduğunu göz ardı edin (nullable ints'ı birlikte ekleyebilir ve her ikisi de null olursa null alabilirsiniz). Diyelim ki böyle bir özellik yok ve sadece özel bir sihri olmayan kullanıcı tanımlı bir sınıf. Mesele şu ki, Bindbir değişkeni Nullabledeğerimizin içeriğine bağlamak için işlevi kullanabilir ve sonra garip bir şey olmadığını iddia edebiliriz ve bunları normal ints gibi kullanabiliriz ve birlikte ekleyebiliriz. Biz sonunda null sonucu sarın ve bu null ya (eğer herhangi boş olacak f, gya da hdöner null) veya toplanmasının sonucu olacak f, gvehbirlikte. (bu, bir veritabanındaki bir satırı LINQ'daki bir değişkene nasıl bağlayabileceğimize benzer ve Bindoperatörün değişkenin yalnızca geçerli satır değerlerinden geçirileceğinden emin olacağı bilgisiyle güvenlidir .

Buna oynamak ve herhangi değiştirebilir f, gve hreturn null etmek ve her şey return null göreceksiniz.

Bu nedenle açıkça, bağlayıcı operatörün bu kontrolü bizim için yapması ve bir null değerle karşılaşması durumunda null döndürmesini kurtarması ve aksi takdirde Nullableyapının içindeki değer boyunca lambda'ya geçmesi gerekir .

İşte Bindoperatör:

public static Nullable<B> Bind<A,B>( this Nullable<A> a, Func<A,Nullable<B>> f ) 
    where B : struct 
    where A : struct
{
    return a.HasValue ? f(a.Value) : null;
}

Buradaki türler videodaki gibidir. Bir sürer M a ( Nullable<A>bu durum için C # sözdizimi) ve bir işlev aiçin M b( Func<A, Nullable<B>>C # sözdizimi) ve bir döner M b ( Nullable<B>).

Kod, null değerinin bir değer içerip içermediğini kontrol eder ve eğer öyleyse onu ayıklar ve işleve iletir, aksi takdirde null değerini döndürür. Bu, Bindoperatörün bizim için tüm boş kontrol mantığını işleyeceği anlamına gelir . Yalnızca çağırdığımız değer Bindnull değilse, bu değer lambda işlevine "iletilir", aksi takdirde erken kurtarırız ve tüm ifade null olur. Bu bizim, biz sadece kullanmak bu boş denetimi davranışının tamamen özgür olmak monad kullanan yazma o kodu verir Bind(ve monadic değere içindeki değere bağlı bir değişken olsun fval, gvalve hvalörnek kodda) ve onlara güvenli kullanabilirsiniz Bindonları geçmeden önce null olup olmadığını kontrol etmeye özen gösterecek bilgi .

Bir monad ile yapabileceğiniz şeylere başka örnekler de var. Örneğin, Bindoperatörün bir girdi karakter akışıyla ilgilenmesini sağlayabilir ve ayrıştırıcı birleştiricileri yazmak için kullanabilirsiniz. Her ayrıştırıcı birleştirici, daha sonra geri izleme, ayrıştırıcı hataları vb Bind. zor bitler. Daha sonra belki biri monad'a günlük ekler, ancak monad'ı kullanan kod değişmez, çünkü tüm sihir Bindoperatörün tanımında gerçekleşir, kodun geri kalanı değişmez.

Son olarak, Haskell'de aynı kodun uygulanması ( -- bir yorum satırına başlar).

-- Here's the data type, it's either nothing, or "Just" a value
-- this is in the standard library
data Maybe a = Nothing | Just a

-- The bind operator for Nothing
Nothing >>= f = Nothing
-- The bind operator for Just x
Just x >>= f = f x

-- the "unit", called "return"
return = Just

-- The sample code using the lambda syntax
-- that Brian showed
z = f >>= ( \fval ->
     g >>= ( \gval ->  
     h >>= ( \hval -> return (fval+gval+hval ) ) ) )

-- The following is exactly the same as the three lines above
z2 = do 
   fval <- f
   gval <- g
   hval <- h
   return (fval+gval+hval)

Gördüğünüz gibi dosonunda güzel notasyon düz zorunlu kod gibi görünmesini sağlar. Ve aslında bu tasarım gereğidir. Monadlar, tüm yararlı şeyleri zorunlu programlamada (değiştirilebilir durum, IO vb.) Kapsüllemek için kullanılabilir ve bu güzel zorunlu benzeri sözdizimini kullanarak kullanılabilir, ancak perdelerin arkasında, hepsi sadece monadlar ve bağlama operatörünün akıllı bir uygulamasıdır! Serin şey uygulayarak kendi monads uygulamak olmasıdır >>=ve return. Ve bunu yaparsanız, bu monadlar dogösterimi de kullanabilecektir , bu da sadece iki işlevi tanımlayarak kendi küçük dillerinizi yazabileceğiniz anlamına gelir!


3
Şahsen F # 'ın monad versiyonunu tercih ederim, ama her iki durumda da harikalar.
ChaosPandion

3
Buraya geri geldiğiniz ve yayınınızı güncellediğiniz için teşekkür ederiz. Belirli bir alana bakan programcıların, sadece "teknolojide x nasıl yaparım" yerine sahip olmak yerine, programcıların söz konusu bölgeye nihayetinde nasıl saygı duyduklarını gerçekten anlamalarına yardımcı olan bu tür takiplerdir. Adamsın!
kappasims

Temelde aynı yolu izledim ve monad'ları anlayan aynı yere vardım, bunun bir monad'ın zorunlu bir geliştirici için gördüğüm bağlayıcı davranışının en iyi açıklaması olduğunu söyledi. Her ne kadar yukarıda sth ile biraz daha açıklanmış monads hakkında her şeye dokunmuyorum düşünüyorum.
Jimmy Hoffa

@Jimmy Hoffa - şüphesiz haklısın. Onları gerçekten daha derinden anlamayı düşünüyorum, en iyi yol onları çok kullanmaya başlamak ve deneyim kazanmak . Henüz bu fırsatım olmadı, ama umarım yakında olur.
Charlie Flowers

Monad, programlama açısından sadece daha yüksek bir soyutlama seviyesi gibi görünüyor ya da sadece matematikte sürekli ve ayırt edilemeyen bir fonksiyon tanımı. Her iki durumda da, özellikle matematikte yeni bir kavram değiller.
liang

11

Bir monad esasen ertelenmiş işlemdir. Yan etkilere (örneğin G / Ç) sahip kodlara izin vermeyen ve yalnızca saf hesaplamaya izin veren bir dilde yazmaya çalışıyorsanız, bir dodge "Tamam, yan etkiler yapmayacağınızı biliyorum benim için, ama eğer yaparsan ne olacağını hesaplayabilir misin? "

Bir tür hile.

Şimdi, bu açıklama monadların büyük resim niyetini anlamanıza yardımcı olacaktır, ancak şeytan ayrıntılarda gizlidir. Tam olarak nasıl yapmak Sonuçlarını hesaplamak? Bazen hoş değil.

Zorunlu programlamaya alışkın olan biri için nasıl bir genel bakış vermenin en iyi yolu, sizi bir DSL'ye yerleştirdiğini söylemektir. (örneğin) bir çıktı dosyasına yazabiliyorsanız ne istiyorsunuz? Neredeyse (ama gerçekten değil) sanki daha sonra değerlendirilmek üzere bir dizede kod oluşturuyordunuz.


1
I Robot kitabındaki gibi mi? Bilim adamı bir bilgisayardan uzay yolculuğunu hesaplamasını ve onlardan belirli kuralları atlamasını istemesini ister mi? :) :) :) :)
OscarRyz

3
Hmm, A Monad , ertelenmiş işleme ve yan etki işlevlerini kapsüllemek için kullanılabilir, aslında Haskell'deki ilk gerçek uygulama, ancak aslında çok daha genel bir modeldi. Diğer yaygın kullanımlar hata işleme ve durum yönetimini içerir. Sözdizimsel şeker (Haskell'de yapmak, F #'da Hesaplama İfadeleri, C #'da Linq sözdizimi) sadece bu ve Monads için temeldir.
Mike Hadlow

@MikeHadlow: Hata işleme ( Maybeve Either e) ve durum yönetimi ( State s, ST s) için monad örnekleri bana, "Lütfen [benim için yan etkiler] yaparsanız ne olacağını hesaplayın" özel örnekleri olarak vurur. Başka bir örnek de belirsizlik ( []) olacaktır.
15'te pyon

bu tam olarak doğru; her bir "monadik" değer, potansiyel olarak saf olmayan bir "hesaplama" anlamına gelen "saf" dilinizin kendisinin geçerli bir değeri olduğundan , E DSL, yani gömülü DSL olduğu bir (iyi, iki) eklemeyle. Ayrıca, zincir saf olanak sağlar saf bir dilde monadik "Bağlanma" yapı exist kurucular , her biri bir önceki hesaplama, sonucu ile çağrılır bu değerlerin tamamı kombine hesaplama "çalışma" dir. Bu, gelecekteki sonuçlara (veya her durumda ayrı, "çalıştırma" zaman çizelgesine) sahip olabileceğimiz anlamına gelir .
Will Ness

ancak bir programcı için bu, EDSL'de saf dilimizin saf hesaplamaları ile karıştırırken programlayabileceğimiz anlamına gelir. çok katmanlı sandviç yığını çok katmanlı bir sandviçtir. bu kadar basit.
Ness

4

Diğer kullanıcıların derinlemesine yayınlayacağından eminim, ancak bu videoyu bir ölçüde yararlı buldum , ancak hala çözüme başlayabileceğim (veya yapmalıyım) konsepti ile hala akıcı bir şekilde olmadığımı söyleyeceğim. Monads ile sezgisel sorunlar.


1
Daha da yararlı bulduğum videonun altında bir C # örneği içeren yorum oldu.
jalf

Daha yararlı olduğunu bilmiyorum , ama kesinlikle fikirleri uygulamaya koydum.
TheMissingLINQ

0

Bir monad'ı , sınıfların uygulamak zorunda olduğu bir C #interface olarak düşünebilirsiniz . Bu, arayüzünüzde bu bildirimleri neden seçmek istediğinizin arkasındaki tüm kategori teorik matematiğini ve yan etkilerden kaçınmaya çalışan bir dilde monad'lara sahip olmak istediğiniz tüm nedenleri göz ardı eden pragmatik bir cevaptır, ama (C #) arayüzlerini anlayan biri olarak iyi bir başlangıç ​​olarak buldum.


Detaylandırabilir misin? Monad'larla ilişkilendiren bir arayüz hakkında ne var?
Joel Coehoorn

2
Blog gönderisinin bu soruya ayrılan birkaç paragrafı geçtiğini düşünüyorum.
hao

0

Cevabımı gör için "Ne monad nedir?"

Motive edici bir örnekle başlar, örnek üzerinden çalışır, bir monad örneği oluşturur ve "monad" ı resmi olarak tanımlar.

İşlevsel programlama hakkında hiçbir bilgi sahibi değildir ve function(argument) := expressionmümkün olan en basit ifadelerle sözdizimi içeren sahte kod kullanır .

Bu C # programı, sözde kod monadının bir uygulamasıdır. (Referans için: Mtür yapıcısı, feed"bağlama" işlemi ve wrap"dönüş" işlemi.)

using System.IO;
using System;

class Program
{
    public class M<A>
    {
        public A val;
        public string messages;
    }

    public static M<B> feed<A, B>(Func<A, M<B>> f, M<A> x)
    {
        M<B> m = f(x.val);
        m.messages = x.messages + m.messages;
        return m;
    }

    public static M<A> wrap<A>(A x)
    {
        M<A> m = new M<A>();
        m.val = x;
        m.messages = "";
        return m;
    }

    public class T {};
    public class U {};
    public class V {};

    public static M<U> g(V x)
    {
        M<U> m = new M<U>();
        m.messages = "called g.\n";
        return m;
    }

    public static M<T> f(U x)
    {
        M<T> m = new M<T>();
        m.messages = "called f.\n";
        return m;
    }

    static void Main()
    {
        V x = new V();
        M<T> m = feed<U, T>(f, feed(g, wrap<V>(x)));
        Console.Write(m.messages);
    }
}
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.