İş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 .