Bir OO programını işlevsel bir programa nasıl yeniden aktive edebilirim?


26

Programları işlevsel bir tarzda yazma konusunda kaynakları bulmakta güçlük çekiyorum. Çevrimiçi olarak tartışılan en gelişmiş konu, sınıf hiyerarşilerini azaltmak için yapısal yazmayı kullanmaktı; çoğu zorunlu döngüleri değiştirmek için map / fold / reduc / etc komutunun nasıl kullanılacağı ile ilgilidir.

Gerçekten bulmak istediğim şey, önemsiz olmayan bir programın ÇOP uygulamasının derinlemesine tartışılması, sınırlamaları ve işlevsel bir biçimde nasıl yeniden yapılandırılacağı. Sadece bir algoritma veya veri yapısı değil, aynı zamanda farklı rollere ve yönlere sahip bir şey - belki de bir video oyunu. Bu arada Tomas Petricek'in Gerçek Dünya Fonksiyonel Programlamasını okudum, ama daha fazlasını istemekten ayrıldım.


6
Bunun mümkün olduğunu sanmıyorum. her şeyi yeniden tasarlamanız (ve yeniden yazmanız) gerekir.
Bryan Chen

18
-1, bu gönderi, OOP ve işlevsel stilin aykırı olduğu yönündeki yanlış varsayımla önyargılıdır. Bunlar çoğunlukla ortogonal kavramlardır ve IMHO onların olmadığı bir efsanedir. “İşlevsel” “Prosedürel” e daha çok karşı çıkar ve her iki stil de OOP ile birlikte kullanılabilir.
Doc Brown,

11
@DocBrown, OOP değişken bir duruma çok fazla güveniyor. Vatansız nesneler mevcut OOP tasarım uygulamasına iyi uymuyor.
SK-mantık

9
@ SK-mantık: anahtar vatansız nesneler değil, değişmez nesnelerdir. Nesneler değiştirilebilir olsalar bile, verilen bağlam içinde değiştirilmediği sürece, sistemin işlevsel bir bölümünde sıklıkla kullanılabilirler. Dahası, sanırım nesneler ve kapaklar birbirinin yerine geçebilir. Yani tüm bunlar OOP ve "işlevsel" in ters olmadıklarını gösteriyor.
Doc Brown,

12
@DocBrown: Ben zihniyetlerin çatışmaya eğilimli olduğunu düşünüyorum, dil yapıları dik olduğunu düşünüyorum. OOP çalışanları “nesneler nedir ve nasıl işbirliği yapıyorlar?”; işlevsel insanlar “verilerim nedir ve onu nasıl dönüştürmek istiyorum?” diye sormaya meyillidir. Bunlar aynı sorular değildir ve farklı cevaplara götürürler. Ayrıca soruyu yanlış anladığını da düşünüyorum. Bu, "OOP 'in sürtüşmesi ve FP kuralları, OOP’dan nasıl kurtulurum?", "OOP alıyorum ve FP almıyorum, bir OOP programını işlevsel bir programa dönüştürmenin bir yolu var, bu yüzden alabilirim. biraz içgörü? "
Michael Shaw,

Yanıtlar:


31

İşlevsel Programlamanın Tanımı

Clojure'un Sevincine Giriş şöyle yazıyor:

İşlevsel programlama, amorf bir tanımı olan hesaplama terimlerinden biridir. Eğer 100 programcıya tanımlarını sorarsanız, muhtemelen 100 farklı cevap alırsınız ...

İşlevsel programlama, işlevlerin uygulanmasını ve bileşimini ilgilendirir ve kolaylaştırır ... İşlevsel olarak kabul edilecek bir dil için, işlev kavramı birinci sınıf olmalıdır. Birinci sınıf işlevler, diğer tüm veriler gibi saklanabilir, iletilebilir ve geri gönderilebilir. Bu temel kavramın ötesinde, [FP'nin tanımları] saflık, değişmezlik, özyineleme, tembellik ve referans saydamlığı içerebilir.

Scala 2nd Edition'da Programlama s. 10 aşağıdaki tanımlara sahiptir:

Fonksiyonel programlama iki ana fikir tarafından yönlendirilir. İlk fikir, işlevlerin birinci sınıf değerler olduğu ... İşlevleri diğer işlevlere argüman olarak iletebilir, onları işlevlerden sonuç olarak döndürebilir veya değişkenlere kaydedebilirsiniz ...

İşlevsel programlamanın ikinci ana fikri, bir programın işlemlerinin, yerinde verileri değiştirmek yerine girdi değerlerini çıktı değerlerine eşlemesi gerektiğidir.

İlk tanımı kabul edersek, kodunuzu "işlevsel" hale getirmek için yapmanız gereken tek şey döngülerinizi tersine çevirmektir. İkinci tanım değişmezliği içerir.

Birinci Sınıf İşlevleri

Şu anda Bus nesnenizden bir Yolcu Listesi aldığınızı ve bunun her bir yolcunun banka hesabını otobüs ücretinin miktarına düşürdüğünü yinelediğinizi hayal edin. Bu aynı işlemi gerçekleştirmenin işlevsel yolu Bus'ta belki de tek bir argüman işlevini alan HerPassenger için çağrılan bir yöntem kullanmak olacaktır. O zaman Otobüs yolcuları üzerinde yinelemeye devam eder, ancak bu en iyi şekilde başarılır ve yolculuk ücretini talep eden müşteri kodunuz bir fonksiyona konur ve EachPassenger'a iletilir. İşte bu kadar! İşlevsel programlama kullanıyorsunuz.

Zorunlu:

for (Passenger p : Bus.getPassengers()) {
    p.debit(fare);
}

İşlevsel (Scala'da adsız bir işlev veya "lambda" kullanarak):

myBus = myBus.forEachPassenger(p:Passenger -> { p.debit(fare) })

Daha şekerli Scala versiyonu:

myBus = myBus.forEachPassenger(_.debit(fare))

Birinci Sınıf Olmayan İşlevler

Diliniz birinci sınıf fonksiyonları desteklemiyorsa, bu çok çirkinleşebilir. Java 7 veya daha önceki sürümlerinde şöyle bir "İşlevsel Nesne" arabirimi sağlamanız gerekir:

// Java 8 has java.util.function.Consumer, but in earlier
// versions you have to roll your own:
public interface Consumer<T> {
    public void accept(T t);
}

Sonra Bus sınıfı dahili bir yineleyici sağlar:

public void forEachPassenger(Consumer<Passenger> c) {
    for (Passenger p : passengers) {
        c.accept(p);
    }
}

Son olarak, anonim bir işlev nesnesini Veri Yolu'na iletin:

// Java 8 has syntactic sugar to make this look more like
// the Scala solution, but earlier versions require manually
// instantiating a "Function Object," in this case, a
// Consumer:
Bus.forEachPassenger(new Consumer<Passenger>() {
    @Override
    public void accept(final Passenger p) {
        p.debit(fare);
    }
}

Java 8, yerel değişkenlerin anonim bir işlevin kapsamına alınmasına izin verir, ancak önceki sürümlerde, bu tür değişkenlerin son olarak bildirilmesi gerekir. Bunu aşmak için bir MutableReference sarmalayıcı sınıfı yapmanız gerekebilir. İşte yukarıdaki koda bir döngü sayacı eklemenizi sağlayan bir tamsayıya özgü sınıf:

public static class MutableIntWrapper {
    private int i;
    private MutableIntWrapper(int in) { i = in; }
    public static MutableIntWrapper ofZero() {
        return new MutableIntWrapper(0);
    }
    public int value() { return i; }
    public void increment() { i++; }
}

final MutableIntWrapper count = MutableIntWrapper.ofZero();
Bus.forEachPassenger(new Consumer<Passenger>() {
    @Override
    public void accept(final Passenger p) {
        p.debit(fare);
        count.increment();
    }
}

System.out.println(count.value());

Bu çirkinlikle bile, dahili bir yineleyici sağlayarak program boyunca yayılan döngülerden karmaşık ve tekrarlanan mantığı ortadan kaldırmak yararlı olabilir.

Bu çirkinlik Java 8'de düzeltildi, ancak birinci sınıf bir işlev içindeki kontrol edilen istisnaları ele almak hala çok çirkin ve Java hala tüm koleksiyonlarında değişkenlik varsayımını taşıyor. Bu bizi genellikle FP ile ilgili diğer hedeflere getiriyor:

değişmezlik

Josh Bloch'un 13. Maddesi "Tercih Edilmezliği Tercih Et" dir. Ortak çöp konuşmasının aksine, OOP değişmez nesnelerle yapılabilir ve bunu yapmak çok daha iyi hale getirir. Örneğin, Java’daki String değişmez. StringBuffer, OTOH'ın sabit bir String oluşturmak için değişken olması gerekir. Tamponlarla çalışmak gibi bazı görevler doğal olarak değişkenlik gerektirir.

Saflık

Her fonksiyon en azından hafızaya alınmalıdır - aynı girdi parametrelerini verirseniz (ve gerçek argümanlarından başka bir girişi olmamalıysa), her defasında aynı durumu, genel durumu değiştirme gibi "yan etkilere" neden olmadan üretmelidir. / O veya istisnalar atma.

İşlevsel Programlama'da, “işi yapmak için genellikle bazı kötülüklerin gerekli olduğu” söylenir. % 100 saflık genellikle amaç değildir. Yan etkilerin en aza indirilmesi.

Sonuç

Gerçekten, yukarıdaki tüm fikirlerden, değişmezlik, kodumu basitleştirmek için pratik uygulamalar açısından en büyük tek galibiyet oldu - ister OOP, ister FP. Fonksiyonları yineleyicilere geçirmek en büyük ikinci galibiyettir. Java 8 Lambda'lar dokümantasyon neden en iyi açıklaması var. Özyineleme ağaçları işlemek için mükemmeldir. Tembellik, sonsuz koleksiyonlarla çalışmanıza izin verir.

JVM'den hoşlanıyorsanız, Scala ve Clojure'a bir göz atmanızı öneririm. Her ikisi de İşlevsel Programlamanın içgörülü yorumudur. Scala bir tür C-benzeri sözdizimiyle tip güvendedir, ancak Haskell ile C ile ortak olarak gerçekten çok fazla sözdizimine sahiptir. Kısa bir süre önce belirli bir yeniden düzenleme problemiyle ilgili olarak Java, Scala ve Clojure karşılaştırmasını yayınladım . Logan Campbell'ın Life of Game'i kullanarak karşılaştırması , Haskell ve ayrıca Clojure yazıyor.

PS

Jimmy Hoffa Bus sınıfımın değişken olduğunu belirtti. Orijinali düzeltmek yerine, bunun tam olarak bu soruyu yeniden şekillendirmenin nasıl bir şey olduğunu göstereceğini düşünüyorum. Bu, Bus'taki her yöntemin yeni bir Otobüs üretmek için bir fabrika, her bir Yolcu'da yeni bir Yolcu üretmek için bir fabrika yapılmasıyla düzeltilebilir. Böylece her şeye bir geri dönüş türü ekledim, bu sayede Tüketici arayüzü yerine Java 8'in java.util.function.Function dosyasını kopyalayacağım.

public interface Function<T,R> {
    public R apply(T t);
    // Note: I'm leaving out Java 8's compose() method here for simplicity
}

Sonra otobüste:

public Bus mapPassengers(Function<Passenger,Passenger> c) {
    // I have to use a mutable collection internally because Java
    // does not have immutable collections that return modified copies
    // of themselves the way the Clojure and Scala collections do.
    List<Passenger> newPassengers = new ArrayList(passengers.size());
    for (Passenger p : passengers) {
        newPassengers.add(c.apply(p));
    }
    return Bus.of(driver, Collections.unmodifiableList(passengers));
}

Son olarak, anonim işlev nesnesi, nesnelerin değiştirilmiş halini döndürür (yeni yolculara sahip yeni bir otobüs). Bu, p.debit () öğesinin şimdi orijinalinden daha az paraya sahip yeni bir değişmez Yolcu döndürdüğünü varsayar:

Bus b = b.mapPassengers(new Function<Passenger,Passenger>() {
    @Override
    public Passenger apply(final Passenger p) {
        return p.debit(fare);
    }
}

Umarım, artık zorunlu dilinizi ne kadar işlevsel hale getirmek istediğinize ilişkin kendi kararınızı verebilir ve projenizi işlevsel bir dil kullanarak yeniden tasarlamanın daha iyi olup olmayacağına karar verebilirsiniz. Scala veya Clojure'da koleksiyonlar ve diğer API'ler, işlevsel programlamayı kolaylaştırmak için tasarlanmıştır. Her ikisi de çok iyi Java birlikte çalışabilir, böylece dilleri karıştırabilir ve eşleştirebilirsiniz. Aslında, Java birlikte çalışabilirliği için Scala, birinci sınıf işlevlerini Java 8 işlevsel arabirimleriyle neredeyse uyumlu olan adsız sınıflarla derler. Derinlik bölümündeki Scala'daki detayları okuyabilirsiniz . 1.3.2 .


Bu cevaptaki çabayı, organizasyonu ve açık iletişimi takdir ediyorum; ancak bazı tekniklerle ilgili biraz sorunum var. Üstte belirtildiği gibi anahtarlardan biri işlevlerin bileşimidir; bu, nesnelerin içinde büyük ölçüde kapsüllenen işlevlerin neden bir amaç yaratmadığına dayanır: Bir işlev bir nesnenin içinde ise, o nesne üzerinde hareket etmek için orada bulunmalıdır; ve eğer o nesne üzerinde hareket ederse, içselliğini değiştirmesi gerekir. Şimdi, herkesin referans şeffaflığı veya değişmezliği gerektirmediğini, ancak nesneyi yerinden değiştirirse artık geri dönmesi gerekmediğini affedeceğim
Jimmy Hoffa

Ve bir fonksiyon bir değer vermez döndürmez, bir anda fonksiyon başkaları ile birlikte olamaz ve işlevsel kompozisyonun tüm soyutlamasını kaybedersiniz. İşlevin nesneyi yerinde değiştirmesini ve ardından nesneyi döndürmesini sağlayabilirsiniz, ancak bunu yapıyorsa neden yalnızca işlevi nesneyi parametre olarak alıp onu üst nesnesinin sınırlarından kurtarmayacaksınız? Üst nesneden kurtularak, diğer tiplerde de çalışabilecek , bu da FP'nin eksik kalacağınız önemli bir parçasıdır: Soyutlama yazın. forEachPasenger'ınız sadece yolculara karşı çalışır ...
Jimmy Hoffa

1
Haritalandırmak ve azaltmak için şeyleri soyutlamanızın ve bu fonksiyonların nesneleri içerme zorunluluğunun bulunmaması, parametrik polimorfizm yoluyla sayısız tipte kullanılabilmeleridir. OOP dillerinde bulamayacağınız bu soyutlamaların, FP'yi gerçekten tanımlayan ve değere sahip olmasını sağlayan bir birleşmesidir. FP oluşturmak için tembellik, referans saydamlığı, değişmezlik, hatta HM tipi sistem gerekli değildir, bunlar işlevlerin genellikle türler üzerinde soyutlayabildiği işlevsel kompozisyon için amaçlanan dilleri yaratmanın yan etkileridir
Jimmy Hoffa,

@JimmyHoffa Örneğime oldukça adil bir eleştiri yaptınız. Java8 Tüketici arayüzü tarafından değişkenliğe baştan çıkarıldım. Ayrıca FP'nin chouser / fogus tanımı değişmezliği içermiyordu ve daha sonra Odersky / Spoon / Venners tanımını ekledim. Orijinal örneği bıraktım ama altındaki "PS" bölümünün altına yeni ve değişmeyen bir sürüm ekledim. Çirkin Ancak bence orijinallerin içindekileri değiştirmek yerine, yeni nesneler üretmek için nesnelere etki eden işlevler gösteriliyor. Harika yorum!
GlenPeterson

1
Bu konuşma Beyaz Tahtada
GlenPeterson 16.03.2015

12

Bunu "başarmak" konusunda kişisel deneyimim var. Sonunda, tamamen işlevsel bir şeyle gelmedim, ama mutlu olduğum bir şeyle geldim. İşte nasıl yaptım:

  • Tüm dış durumları işlevin bir parametresine dönüştürün. EG: eğer bir nesnenin yöntemi değişirse x, onu xçağırmak yerine yöntemin geçmesini sağlayın this.x.
  • Davranışları nesnelerden kaldırın.
    1. Nesne verilerinin herkese açık olarak erişilebilir olmasını sağlayın
    2. Tüm yöntemleri, nesnenin çağırdığı işlevlere dönüştürün.
    3. Nesne verilerini arayarak, nesneyi çağıran istemci kodunu yeni işlevi çağırın. EG: dönüştürme x.methodThatModifiesTheFooVar()içinefooFn(x.foo)
    4. Orijinal yöntemi nesneden kaldırın
  • Mümkün olduğu dereceden fonksiyonlar gibi daha yüksek olan birçok tekrarlı döngüler gibi değiştirin map, reduce, filtervb

Değişken durumdan kurtulamadım. Benim dilimde sadece aptalca değildi (JavaScript). Ancak, tüm durumların girip / veya geri verilmesiyle her fonksiyonun test edilmesi mümkündür. Bu, devletin kurulmasının çok uzun süreceği veya bağımlılıkları ayırmanın genellikle üretim kodunu değiştirmeyi gerektiren OOP'tan farklıdır.

Ayrıca tanım konusunda yanılmış olabilirim, ancak işlevlerimin referans açısından şeffaf olduğunu düşünüyorum: İşlevlerim aynı girdiyi verdiğinde aynı etkiye sahip olacak.

Düzenle

Burada görebileceğiniz gibi , JavaScript'te gerçekten değişmez bir nesne oluşturmak mümkün değildir. Çalışkansanız ve kodunuzu kimin aradığını kontrol ediyorsanız, mevcut kodu değiştirmek yerine her zaman yeni bir nesne oluşturarak bunu yapabilirsiniz. Benim için çabaya değmezdi.

Ancak Java kullanıyorsanız, bu teknikleri sınıflarınızı değişmez kılmak için kullanabilirsiniz .


+1 Tam olarak ne yapmaya çalıştığınıza bağlı olarak, bu muhtemelen sadece "yeniden yapılanmanın" ötesine geçecek tasarım değişiklikleri yapmadan gidebileceğiniz kadarıyla mümkün.
Evicatos

@Evicatos: JavaScript'in değişmez durum için daha iyi desteği olsaydı bilmiyorum, çözümüm Clojure gibi dinamik bir işlevsel dilde alacağınız kadar işlevsel olacağını düşünüyorum. Sadece yeniden yapılanmanın ötesinde bir şey gerektiren bir şeye bir örnek nedir?
Daniel Kaplan

Bence değişebilir devletten kurtulmak yeter. Bunun sadece dilde daha iyi bir destek meselesi olduğunu sanmıyorum, değişebilirden değişmezliğe geçmenin temelde her zaman temelde bir yeniden yazmak olan temel mimari değişiklikler gerektireceğini düşünüyorum. Yine de yeniden yönlendirme tanımınıza bağlı olarak Ymmv.
Evicatos

@Evicatos düzenlememi gör
Daniel Kaplan

1
@tieTYT evet, JS'nin bu kadar değişken olması üzücü ama en azından Clojure JavaScript'i derleyebilir: github.com/clojure/clojurescript
GlenPeterson

3

Programı tamamen yeniden düzenlemenin gerçekten mümkün olduğunu sanmıyorum - doğru paradigmada yeniden tasarlamanız ve yeniden uygulamanız gerekir.

Kod yeniden yapılandırmanın "mevcut davranış kodunu yeniden yapılandırmak, dış yapısını değiştirmeksizin iç yapısını değiştirmek için disiplinli bir teknik" olarak tanımladığını gördüm.

Bazı şeyleri daha işlevsel hale getirebilirsiniz, fakat özünde hala nesneye yönelik bir programınız var. Farklı parçalara paradigmaya uyarlamak için küçük parça ve parçaları değiştiremezsiniz.


Referans bir şeffaflık için gayret etmek için iyi bir ilk işaret olduğunu ekleyeceğim. Bunu elde ettikten sonra, işlevsel programlamanın avantajlarından ~% 50 elde edersiniz.
Daniel Gratzer

3

Bence bu yazı dizisi tam olarak istediğin şey:

Tamamen İşlevsel Retro Oyunlar

http://prog21.dadgum.com/23.html Bölüm 1

http://prog21.dadgum.com/24.html 2. Bölüm

http://prog21.dadgum.com/25.html 3. Bölüm

http://prog21.dadgum.com/26.html Bölüm 4

http://prog21.dadgum.com/37.html Takip

Özet:

Yazar, yan etkileri olan bir ana döngü önerir (yan etkiler bir yerde olmalı, doğru mu?) Ve çoğu işlev, oyunun durumunu nasıl değiştirdiklerini ayrıntılandıran küçük değişken kayıtları döndürür.

Elbette, gerçek dünyadaki bir program yazarken, her birini en çok yardımcı olan kullanarak, birkaç programlama stilini karıştıracak ve eşleştireceksiniz. Bununla birlikte, bir programı en işlevsel / değişmez şekilde yazmayı ve aynı zamanda en spagetti biçiminde yazmayı denemek, yalnızca global değişkenleri kullanarak denemek iyi bir deneyimdi.


2

OOP ve FP'nin kodu düzenlemede iki zıt yaklaşıma sahip olması nedeniyle muhtemelen tüm kodunuzu tersine çevirmeniz gerekir.

OOP, türler (sınıflar) etrafında kod düzenler: farklı sınıflar aynı işlemi uygulayabilir (aynı imzayla bir yöntem). Sonuç olarak, operasyonlar çok fazla değişmediğinde OOP daha uygun olurken, yeni tipler çok sık eklenebilir. Örneğin, her bir eklendi yöntem sabit bir dizi olan bir GUI kütüphanesi düşünün ( hide(), show(), paint(), move()ve böyle devam eder), fakat kütüphane uzatılır yeni aletler ilave edilebilir. OOP'de yeni bir tür eklemek kolaydır (belirli bir arabirim için): yalnızca yeni bir sınıf eklemeniz ve tüm yöntemlerini uygulamanız gerekir (yerel kod değişikliği). Öte yandan, bir arabirime yeni bir işlem (yöntem) eklemek, bu arabirimi uygulayan tüm sınıfların değiştirilmesini gerektirebilir (kalıtım iş miktarını azaltsa bile).

FP, işlemler (işlevler) etrafında kod düzenler: her işlev, farklı türlere farklı şekillerde davranabilecek bazı işlemler uygular. Bu genellikle tip eşleştirmesi veya başka bir mekanizma yoluyla türün gönderilmesiyle elde edilir. Sonuç olarak, FP tipleri tipler sabit olduğunda ve yeni işlemler daha sık eklendiğinde daha uygundur. Örneğin sabit bir dizi görüntü formatını (GIF, JPEG, vb.) Ve uygulamak istediğiniz bazı algoritmaları kullanın. Her algoritma, görüntünün türüne göre farklı davranış gösteren bir fonksiyonla uygulanabilir. Yeni bir algoritma eklemek kolaydır, çünkü yalnızca yeni bir işlev uygulamanız gerekir (yerel kod değişikliği). Yeni bir biçim (tür) eklemek, şimdiye kadar uyguladığınız tüm işlevleri değiştirmeyi gerektirir (yerel olmayan değişiklik).

Alt satır: OOP ve FP, kod düzenleme biçimleri bakımından temelde farklıdır ve bir OOP tasarımını FP tasarımına dönüştürmek, tüm kodunuzu bunu yansıtacak şekilde değiştirmeyi içerir. Yine de ilginç bir alıştırma olabilir. Ayrıca Mikemay tarafından belirtilen SICP kitabındaki bu ders notlarına , özellikle 13.1.5 ile 13.1.10 arasındaki slaytlara bakınız.

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.