Bir arabirimin (OOP) anlamsal sözleşmesi bir işlev imzasından (FP) daha bilgilendirici midir?


16

Bazıları tarafından, eğer SOLID ilkelerini aşırı uçlarına götürürseniz, fonksiyonel programlamaya son verdiğiniz söylenir . Bu makaleye katılıyorum, ancak arayüz / nesneden işlev / kapanışa geçişte bazı anlambilimin kaybolduğunu düşünüyorum ve İşlevsel Programlamanın kaybı nasıl azaltabileceğini bilmek istiyorum.

Makaleden:

Ayrıca, Arabirim Ayrışma İlkesini (ISS) titizlikle uygularsanız, Başlık Arabirimleri yerine Rol Arabirimleri'ni tercih etmeniz gerektiğini anlayacaksınız.

Tasarımınızı daha küçük ve daha küçük arayüzlere doğru sürmeye devam ederseniz, en sonunda nihai Rol Arayüzüne ulaşırsınız: tek bir yöntemle bir arayüz. Bu bana çok şey oluyor. İşte bir örnek:

public interface IMessageQuery
{
    string Read(int id);
}

Bir bağımlılık alırsamIMessageQuery , örtük sözleşmenin bir parçası, çağrı Read(id), verilen kimliğe sahip bir mesaj arayacak ve döndürecektir.

Bunu, eşdeğer işlevsel imzasına bağımlı olmakla karşılaştırın int -> string. Herhangi bir ek ipucu olmadan, bu işlev basit olabilir ToString(). Eğer IMessageQuery.Read(int id)bir ile uyguladıysan , ToString()seni kasten yıkıcı olmakla suçlayabilirim!

Peki, işlevsel programcılar iyi adlandırılmış bir arabirimin anlambilimini korumak için ne yapabilir? Örneğin, tek üyeli bir kayıt türü oluşturmak geleneksel midir?

type MessageQuery = {
    Read: int -> string
}

5
Bir OOP arayüzü tek bir işlev değil, FP tip sınıfı gibidir .
9000

2
Çok fazla iyi bir şey olabilir, ISS'yi 'titizlikle' uygulamak, arayüz başına 1 yöntemle sonlanmak için çok ileri gidiyor.
gbjbaanb

3
@gbjbaanb aslında arayüzlerimin çoğunun birçok uygulaması olan tek bir yöntemi var. SOLID ilkelerini ne kadar çok uygularsanız, faydaları o kadar çok görebilirsiniz. Ama bu bu soru için konu dışı
AlexFoxGill

1
@jk .: Eh, Haskell'de, bu tür sınıflar, OCaml'de bir modül ya da bir işlev kullanırsınız, Clojure'da bir protokol kullanırsınız. Her durumda, arayüz benzetmenizi genellikle tek bir işlevle sınırlamazsınız.
9000

2
Without any additional clues... belki de belgelerin sözleşmenin bir parçası olmasının nedeni budur ?
SJuan76

Yanıtlar:


8

Telastyn'in dediği gibi, işlevlerin statik tanımlarını karşılaştırmak:

public string Read(int id) { /*...*/ }

için

let read (id:int) = //...

OOP'tan FP'ye giden hiçbir şeyi gerçekten kaybetmediniz.

Bununla birlikte, bu hikayenin sadece bir parçasıdır, çünkü işlevler ve arayüzler sadece statik tanımlarında belirtilmez. Onlar da etrafta dolaşıyorlar . Diyelim ki MessageQuerybaşka bir kod parçası tarafından okundu, a MessageProcessor. Sonra elimizde:

public void ProcessMessage(int messageId, IMessageQuery messageReader) { /*...*/ }

Şimdi yöntem adını veya parametresini doğrudan göremiyoruz , ancak oraya IDE'mizden kolayca ulaşabiliriz. Daha genel olarak, int'den dizeye bir işlev içeren bir yöntemle yalnızca herhangi bir arabirimden geçmemiz, bu parametre adı meta verilerini bu işlevle ilişkili tuttuğumuz anlamına gelir .IMessageQuery.Readint idIMessageQueryid

Öte yandan, fonksiyonel versiyonumuz için:

let read (id:int) (messageReader : int -> string) = // ...

Peki ne sakladık ve kaybettik? Yine de, messageReadermuhtemelen tür adını (buna eşdeğer IMessageQuery) gereksiz kılan parametre adına sahibiz . Ama şimdi idfonksiyonumuzdaki parametre adını kaybettik .


Bunun etrafında iki ana yol var:

  • İlk olarak, bu imzayı okuduktan sonra, neler olup bittiğini oldukça iyi tahmin edebilirsiniz. İşlevleri kısa, basit ve uyumlu tutarak ve iyi adlandırma kullanarak, bu bilgileri sezmeyi veya bulmayı çok daha kolay hale getirirsiniz. Asıl fonksiyonun kendisini okuduktan sonra, daha da basit olurdu.

  • İkincisi, ilkelleri sarmak için küçük tipler oluşturmak için birçok işlevsel dilde deyimsel tasarım olarak kabul edilir . Bu durumda, tam tersi gerçekleşir - bir tür adını bir parametre adı ( IMessageQueryyerine messageReader) ile değiştirmek yerine, bir parametre adını bir tür adıyla değiştirebiliriz. Örneğin, intşu şekilde adlandırılabilir Id:

    type Id = Id of int
    

    Şimdi readimzamız:

    let read (id:int) (messageReader : Id -> string) = // ...
    

    Bu da daha önce sahip olduğumuz kadar bilgilendirici.

    Bir yan not olarak, bu bize OOP'de sahip olduğumuz bazı derleyici korumasını da sağlar. OOP sürümü sağlanmalıdır Oysa biz özellikle aldı IMessageQuerysadece herhangi bir eski yerine int -> stringişlevi, burada biz bir alıyorsun o benzer (ama farklı) koruması Id -> stringsadece herhangi bir eski yerine int -> string.


% 100 güvenle, bu tekniklerin her zaman bir arayüzde tam bilgiye sahip olmak kadar iyi ve bilgilendirici olacağını söylemek konusunda isteksiz olurum, ancak yukarıdaki örneklerden, çoğu zaman, muhtemelen bir iş kadar iyi yapabilir.


1
Bu benim en sevdiğim cevap - FP'nin semantik tiplerin kullanımını teşvik ettiği
AlexFoxGill

10

FP yaparken daha spesifik semantik tipler kullanmaya eğilimliyim.

Örneğin, benim için yönteminiz şöyle bir şey olurdu:

read: MessageId -> Message

Bu, OO (/ java) tarzı ThingDoer.doThing()stilden çok daha fazla iletişim kurar


2
+1. Ayrıca, Haskell gibi yazılan FP dillerinde yazı sistemine çok daha fazla özellik kodlayabilirsiniz. Bu bir haskell türü olsaydı, hiç IO yapmadığını bilirdim (ve muhtemelen bir yerlerde mesajlara kimliklerin bazı bellek içi eşlemesine atıfta bulunur). OOP dillerinde, bu bilgilere sahip değilim - bu işlev bir veritabanını çağrmanın bir parçası olarak çalabilir.
Jack

1
Bunun neden Java stilinden daha fazla iletişim kuracağı konusunda net değilim . Bunun olmadığını read: MessageId -> Messagesöyleyen ne string MessageReader.GetMessage(int messageId)?
Ben Aaronson

@ BenAaronson sinyal-gürültü oranı daha iyidir. OO örneği yöntemi ise, kaputun altında ne yaptığını bilmiyorsam, ifade edilmeyen her türlü bağımlılığa sahip olabilir. Jack'in belirttiği gibi, daha güçlü tipleriniz olduğunda daha fazla güvenceniz vardır.
Daenyth

9

Peki, işlevsel programcılar iyi adlandırılmış bir arabirimin anlambilimini korumak için ne yapabilir?

İyi adlandırılmış işlevler kullanın.

IMessageQuery::Read: int -> stringbasitçe ReadMessageQuery: int -> stringya da benzer bir şey olur .

Dikkat edilmesi gereken en önemli şey, isimlerin sadece kelimenin en gevşek anlamında sözleşmeler olmasıdır. Yalnızca siz ve başka bir programcı adından aynı sonuçları çıkarırsanız ve onlara itaat ederseniz çalışırlar. Bu nedenle, bu zımni davranışı ileten herhangi bir adı kullanabilirsiniz . OO ve fonksiyonel programlama adları biraz farklı yerlerde ve biraz farklı şekillerde bulunur, ancak işlevleri aynıdır.

Bir arabirimin (OOP) anlamsal sözleşmesi bir işlev imzasından (FP) daha bilgilendirici midir?

Bu örnekte değil. Yukarıda açıkladığım gibi, tek bir işleve sahip tek bir sınıf / arabirim, benzer şekilde iyi adlandırılmış bağımsız bir işleve göre anlamlı olarak daha bilgilendirici değildir.

Bir sınıfta birden fazla işlev / alan / özellik edindikten sonra, bunlar hakkında daha fazla bilgi çıkarabilirsiniz, çünkü ilişkilerini görebilirsiniz. Aynı / benzer parametreleri alan bağımsız işlevlerden veya ad alanı veya modül tarafından düzenlenen bağımsız işlevlerden daha bilgilendirici olup olmadığı tartışılabilir.

Şahsen, OO'nun daha karmaşık örneklerde bile önemli ölçüde daha bilgilendirici olduğunu düşünmüyorum.


2
Parametre adları ne olacak?
Ben Aaronson

@BenAaronson - Peki ya onlar? Hem OO hem de fonksiyonel diller parametre adlarını belirtmenize izin verir, bu yüzden bir itme. Buradaki soru ile tutarlı olmak için burada tip imzası stilini kullanıyorum. Gerçek bir dilde şöyle görünecektirReadMessageQuery id = <code to go fetch based on id>
Telastyn

1
Bir işlev bir arabirimi parametre olarak alırsa, o arabirimde bir yöntemi çağırırsa, arabirim yönteminin parametre adları kolayca kullanılabilir. Bir işlev parametre olarak başka bir işlevi alırsa, işlevde geçen için parametre adları atamak kolay değildir
Ben Aaronson

1
Evet, @ BenAaronson bu işlev imzasında kaybolan bilgi parçalarından biridir. Örneğin let consumer (messageQuery : int -> string) = messageQuery 5, içinde idparametre sadece bir int. Sanırım bir argüman, bir Iddeğil, an geçirmeniz gerektiğidir int. Aslında bu kendi içinde iyi bir cevap olurdu.
AlexFoxGill

@AlexFoxGill Aslında ben sadece bu satır boyunca bir şeyler yazma sürecindeydim!
Ben Aaronson

0

Tek bir işlevin 'anlamsal bir sözleşmeye' sahip olamayacağını kabul etmiyorum. Şu yasaları göz önünde bulundurun foldr:

foldr f z nil = z
foldr f z (singleton x) = f x z
foldr f z (xn <> ys) = foldr f (foldr f z ys) xn

Hangi anlamda anlamsal değil, sözleşme değil? 'Katlayıcı' için bir tür tanımlamanız gerekmez, özellikle foldrbu yasalar tarafından benzersiz bir şekilde belirlenir. Ne yapacağını biliyorsun.

Bir tür işlev almak istiyorsanız, aynı şeyi yapabilirsiniz:

-- The first argument `f` must satisfy for all x, y, z
-- > f x x = true
-- > f x y = true and f y x = true implies x = y
-- > f x y = true and f y z = true implies f x z = true
sort :: forall 'a. (a -> a -> bool.t) -> list.t a -> list.t a;

Bu sözleşmeyi yalnızca aynı sözleşmeye birden çok kez ihtiyacınız varsa adlandırmanız ve yakalamanız gerekir:

-- Type of functions `f` satisfying, for all x, y, z
-- > f x x = true
-- > f x y = true and f y x = true implies x = y
-- > f x y = true and f y z = true implies f x z = true
type comparison.t 'a = a -> a -> bool.t;

Tür denetleyicisi, bir türe atadığınız anlambilimi zorlamayacaktır; bu nedenle, her sözleşme için yeni bir tür oluşturmak yalnızca kaynaktır.


1
İlk noktanızın soruyu ele aldığını sanmıyorum - bu bir işlev tanımı, bir imza değil. Tabii ki bir sınıfın ya da bir fonksiyonun uygulanmasına bakarak ne yaptığını anlayabilirsiniz - soru semantikleri soyutlama seviyesinde hala koruyabileceğinizi sorar (bir arayüz veya fonksiyon imzası)
AlexFoxGill

@AlexFoxGill - benzersiz bir şekilde belirlemesine rağmen, bir işlev tanımı değildirfoldr . İpucu: foldrYukarıda verilen şartnamenin üçü varken , tanımının iki denklemi vardır (neden?).
Jonathan Cast

Demek istediğim, foldr bir işlev imzası değil bir işlevdir. Kanunlar veya tanımlar imzanın bir parçası değil
AlexFoxGill

-1

Hemen hemen tüm statik olarak yazılan fonksiyonel diller, temel türleri semantik niyetinizi açıkça belirtmenizi gerektiren bir şekilde diğer adlara takma yoluna sahiptir. Diğer cevaplardan bazıları örnek vermiştir. Pratikte, deneyimli işlevsel programcıların bu sarıcı tiplerini kullanmak için çok iyi nedenlere ihtiyaçları vardır, çünkü bunlar sıkıştırılabilirliğe ve tekrar kullanılabilirliğe zarar verirler.

Örneğin, bir müşterinin bir liste tarafından desteklenen bir mesaj sorgusunun uygulanmasını istediğini varsayalım. Haskell'de uygulama şu kadar basit olabilir:

messages = ["Message 0", "Message 1", "Message 2"]
messageQuery = (messages !!)

newtype Message = Message StringBunu kullanmak çok daha az açıktır ve bu uygulamayı aşağıdaki gibi bir şey olarak bırakır:

messages = map Message ["Message 0", "Message 1", "Message 2"]
messageQuery (Id index) = messages !! index

Bu büyük bir anlaşma gibi görünmeyebilir, ama ya ileri o tür dönüştürmesi geri yapmak zorunda ve her yerde , ya da yukarıda herşey Kodunuzdaki bir sınır tabakası kurmak Int -> Stringo zaman dönüştürmek Id -> Messageaşağıda tabakasına geçmesine. Diyelim ki uluslararasılaştırma eklemek veya tüm büyük harflerle biçimlendirmek ya da günlük bağlamı ya da başka bir şey eklemek istedim. Bu operasyonların tümü bir ile bestelemek çok basit Int -> Stringve bir ile can sıkıcı Id -> Message. Artan tür kısıtlamalarının istendiği durumlar asla değildir, ancak rahatsızlık değiş tokuşa değer olsa iyi olur.

Bir sarmalayıcı yerine (bunun yerine Haskell'de ) bir tür eş anlamlı kullanabilirsiniz , bu çok daha yaygındır ve her yerde dönüşüm gerektirmez, ancak OOP sürümünüz gibi statik tür garantileri sağlamaz, sadece biraz kapsülleme. Tip sarmalayıcılar çoğunlukla müşterinin değeri değiştirmesi beklenmeyen yerlerde kullanılır, sadece saklayın ve geri verin. Örneğin, bir dosya tanıtıcısı.typenewtype

Hiçbir şey bir istemcinin "yıkıcı" olmasını engelleyemez. Her müşteri için sadece atlamak için çemberler oluşturuyorsun. Ortak bir birim testi için basit bir alay genellikle üretimde mantıklı olmayan garip davranışlar gerektirir . Arayüzleriniz mümkün olduğu takdirde umursamayacakları şekilde yazılmalıdır.


1
Bu daha çok Ben / Daenyth'in cevapları üzerine bir yorum gibi geliyor - anlamsal türleri kullanabiliyorsunuz, ancak rahatsızlıktan dolayı değil mi? Seni küçümsemedim ama bu sorunun cevabı değil.
AlexFoxGill

-1 Kompozisyon veya tekrar kullanılabilirliğe zarar vermez. Eğer Idve Messagebasit sargı olan Intve String, öyle önemsiz aralarında dönüştürün.
Michael Shaw

Evet, önemsiz, ama her yerde yapmak can sıkıcı. Bir örnek ve biraz da tür eşanlamlıları ve sarmalayıcıların kullanımı arasındaki farkı açıkladım.
Karl Bielefeldt

Bu büyük bir şey gibi görünüyor. Küçük projelerde, bu tür sargı tipleri muhtemelen bu kadar yararlı değildir. Büyük projelerde, sınırların genel kodun küçük bir oranı olacağı doğaldır, bu nedenle bu tür dönüşümleri sınırda yapmak ve daha sonra sarılmış türleri başka bir yere aktarmak çok zahmetli değildir. Ayrıca Alex'in dediği gibi, bunun soruya nasıl bir cevap olduğunu anlamıyorum.
Ben Aaronson

Nasıl yapılacağını, fonksiyonel programcıların neden bunu yapmayacağını açıkladım. Bu bir cevap, insanların istediği kişi olmasa bile. Boyut ile ilgisi yoktur. Kodunuzu tekrar kullanmayı zorlaştırmanın bir şekilde iyi bir şey olduğu fikri, kesinlikle bir OOP konsepti. Biraz değer katan bir ürün türü mü oluşturuyorsunuz? Her zaman. Yalnızca tek bir kitaplıkta kullanabilmeniz dışında, yaygın olarak kullanılan bir tür gibi bir tür oluşturmak mı? Belki de OOP kodu ile yoğun bir şekilde etkileşime giren veya çoğunlukla OOP deyimlerine aşina olan biri tarafından yazılan FP kodu hariç, duyulmamış.
Karl Bielefeldt
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.