Değişken durum olmadan faydalı bir şeyi nasıl yapabilirsiniz?


265

Son zamanlarda fonksiyonel programlama hakkında birçok şey okudum ve çoğunu anlayabiliyorum, ama başımı etrafına saramadığım tek şey vatansız kodlama. Bana öyle geliyor ki, değişebilir durumu kaldırarak programlamanın basitleştirilmesi, gösterge tablosunu çıkararak bir arabayı "basitleştirmek" gibidir: bitmiş ürün daha basit olabilir, ancak son kullanıcılarla etkileşime girmesi için iyi şanslar.

Düşündüğüm hemen hemen her kullanıcı uygulaması, devleti temel bir kavram olarak içeriyor. Bir belge (veya SO yazısı) yazarsanız, durum her yeni girişle değişir. Veya bir video oyunu oynarsanız, sürekli hareket etme eğilimi gösteren tüm karakterlerin pozisyonlarından başlayarak tonlarca devlet değişkeni vardır. Değişen değerleri takip etmeden faydalı herhangi bir şeyi nasıl yapabilirsiniz?

Bu konuyu tartışan bir şey bulduğumda, sahip olmadığım ağır bir FP arka planını varsayan gerçekten teknik işlevsel olarak yazılmıştır. Bunu zorunlu kodlama konusunda iyi ve sağlam bir anlayışa sahip birisine açıklamanın bir yolunu bilen biri var, ancak fonksiyonel tarafta tam bir n00b kim var?

DÜZENLEME: Şimdiye kadar verilen yanıtların bir kısmı beni değişmez değerlerin avantajlarından ikna etmeye çalışıyor gibi görünüyor. O kısmı anladım. Mükemmel mantıklı. Anlamadığım değişebilir değişkenler olmadan değişmek ve sürekli değişmek zorunda olan değerleri nasıl takip edebileceğinizdir.


2
Şuna

1
Benim alçakgönüllü görüşüm, bunun güç ve para gibi olduğudur. Azalan getiriler yasası geçerlidir. Oldukça güçlü iseniz, biraz daha güçlü olmak için çok az teşvik olabilir, ancak bunun üzerinde çalışmak zarar vermez (ve bazı insanlar bir tutkuyla yapar). Aynısı küresel değişebilir durum için de geçerlidir. Kodlama becerim ilerledikçe kodumdaki küresel değişebilir durum miktarını sınırlamanın iyi olduğunu kişisel tercihim kabul ediyorum. Asla mükemmel olmayabilir ancak küresel değişebilir durumu en aza indirmek için çalışmak iyidir.
AturSams

Parada olduğu gibi, daha fazla zaman harcayan bir noktaya ulaşılacak, artık çok kullanışlı değil ve diğer öncelikler zirveye çıkacak. Örneğin, mümkün olan en yüksek güce ulaşırsanız (metaforum için), herhangi bir yararlı amaca hizmet etmeyebilir ve hatta bir yük haline gelebilir. Ancak yine de bu ulaşılamaz hedefe doğru çabalamak ve orta düzeyde kaynaklara yatırım yapmak hala iyidir.
AturSams

7
Kısaca, FP'de işlevler hiçbir zaman durumu değiştirmez. Sonunda mevcut durumun yerini alacak bir şey döndürürler . Fakat devlet asla yerinde değiştirilmez (mutasyona uğramaz).
jinglesthula

Mutasyon olmadan durumsallığı elde etmenin yolları vardır (anladığım kadarıyla yığını kullanarak), ancak bu soru noktanın yanında bir anlamda (harika bir soru olmasına rağmen). Özlü hakkında konuşmak zor, ama işte umarım medium.com/@jbmilgrom/… . TLDR, durumsal bir fonksiyonel programın semantiğinin değişmez olduğu, ancak program fonksiyonunun iletişim s / b çalışmalarının ele alındığıdır.
jbmilgrom

Yanıtlar:


166

Veya bir video oyunu oynarsanız, sürekli hareket etme eğilimi gösteren tüm karakterlerin pozisyonlarından başlayarak tonlarca devlet değişkeni vardır. Değişen değerleri takip etmeden faydalı herhangi bir şeyi nasıl yapabilirsiniz?

Eğer ilgileniyorsanız, burada Erlang ile oyun programlama açıklayan bir yazı dizisi.

Muhtemelen bu cevabı beğenmeyeceksiniz, ama siz kullanana kadar fonksiyonel bir program alamayacaksınız . Kod örnekleri gönderebilir ve "İşte, görmüyor musun " diyebilirim - ama sentaksı ve temel prensipleri anlamıyorsan, o zaman gözlerin sırlanır. Sizin bakış açınıza göre, zorunlu bir dil ile aynı şeyi yapıyorum, ancak programlamayı bilerek daha zor hale getirmek için her türlü sınırı ayarlıyorum. Benim açımdan, sadece Blub paradoksunu deneyimliyorsun .

İlk başta şüpheliydim, ama birkaç yıl önce fonksiyonel programlama trenine atladım ve ona aşık oldum. İşlevsel programlama ile ilgili hile, desenleri, belirli değişken atamaları tanıyabilir ve zorunlu durumu yığına taşıyabilir. Örneğin bir for-loop özyineleme olur:

// Imperative
let printTo x =
    for a in 1 .. x do
        printfn "%i" a

// Recursive
let printTo x =
    let rec loop a = if a <= x then printfn "%i" a; loop (a + 1)
    loop 1

Çok hoş değil, ama mutasyon olmadan aynı etkiyi elde ettik. Tabii ki, mümkün olan her yerde, tamamen döngüden kaçınmayı ve soyutlamayı seviyoruz:

// Preferred
let printTo x = seq { 1 .. x } |> Seq.iter (fun a -> printfn "%i" a)

Seq.iter yöntemi koleksiyon boyunca numaralandırılır ve her öğe için anonim işlevi çağırır. Çok kullanışlı :)

Biliyorum, sayıları yazdırmak tam olarak etkileyici değil. Bununla birlikte, oyunlarla aynı yaklaşımı kullanabiliriz: yığındaki tüm durumu tutun ve özyinelemeli çağrıdaki değişikliklerle yeni bir nesne oluşturun. Bu şekilde, her kare, oyunun durumsuz bir anlık görüntüsüdür; burada her kare, vatansız nesnelerin güncellenmesi gereken her türlü değişiklikle birlikte yepyeni bir nesne oluşturur. Bunun sahte kodu şöyle olabilir:

// imperative version
pacman = new pacman(0, 0)
while true
    if key = UP then pacman.y++
    elif key = DOWN then pacman.y--
    elif key = LEFT then pacman.x--
    elif key = UP then pacman.x++
    render(pacman)

// functional version
let rec loop pacman =
    render(pacman)
    let x, y = switch(key)
        case LEFT: pacman.x - 1, pacman.y
        case RIGHT: pacman.x + 1, pacman.y
        case UP: pacman.x, pacman.y - 1
        case DOWN: pacman.x, pacman.y + 1
    loop(new pacman(x, y))

Zorunlu ve işlevsel sürümler aynıdır, ancak işlevsel sürüm açıkça değiştirilebilir bir durum kullanmaz. İşlevsel kod tüm durumu yığında tutar - bu yaklaşımla ilgili güzel şey, bir şeyler ters giderse, hata ayıklamanın kolay olması, ihtiyacınız olan tek şey bir yığın izlemesidir.

Bu, oyundaki herhangi bir sayıda nesneye kadar ölçeklendirilir, çünkü tüm nesneler (veya ilgili nesnelerin koleksiyonları) kendi iş parçacıklarında oluşturulabilir.

Düşündüğüm hemen hemen her kullanıcı uygulaması, devleti temel bir kavram olarak içeriyor.

İşlevsel dillerde, nesnelerin durumunu değiştirmek yerine, istediğimiz değişikliklerle yeni bir nesne döndürürüz. Göründüğünden daha etkilidir. Örneğin veri yapılarının değişmez veri yapıları olarak temsil edilmesi çok kolaydır. Örneğin, yığınların uygulanması son derece kolaydır:

using System;

namespace ConsoleApplication1
{
    static class Stack
    {
        public static Stack<T> Cons<T>(T hd, Stack<T> tl) { return new Stack<T>(hd, tl); }
        public static Stack<T> Append<T>(Stack<T> x, Stack<T> y)
        {
            return x == null ? y : Cons(x.Head, Append(x.Tail, y));
        }
        public static void Iter<T>(Stack<T> x, Action<T> f) { if (x != null) { f(x.Head); Iter(x.Tail, f); } }
    }

    class Stack<T>
    {
        public readonly T Head;
        public readonly Stack<T> Tail;
        public Stack(T hd, Stack<T> tl)
        {
            this.Head = hd;
            this.Tail = tl;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Stack<int> x = Stack.Cons(1, Stack.Cons(2, Stack.Cons(3, Stack.Cons(4, null))));
            Stack<int> y = Stack.Cons(5, Stack.Cons(6, Stack.Cons(7, Stack.Cons(8, null))));
            Stack<int> z = Stack.Append(x, y);
            Stack.Iter(z, a => Console.WriteLine(a));
            Console.ReadKey(true);
        }
    }
}

Yukarıdaki kod iki değişmez liste oluşturur, bunları yeni bir liste yapmak için birleştirir ve sonuçları ekler. Uygulamada hiçbir yerde değiştirilebilir durum kullanılmaz. Biraz hantal görünüyor, ancak bunun nedeni sadece C # 'ın ayrıntılı bir dil olması. İşte F # 'daki eşdeğer program:

type 'a stack =
    | Cons of 'a * 'a stack
    | Nil

let rec append x y =
    match x with
    | Cons(hd, tl) -> Cons(hd, append tl y)
    | Nil -> y

let rec iter f = function
    | Cons(hd, tl) -> f(hd); iter f tl
    | Nil -> ()

let x = Cons(1, Cons(2, Cons(3, Cons(4, Nil))))
let y = Cons(5, Cons(6, Cons(7, Cons(8, Nil))))
let z = append x y
iter (fun a -> printfn "%i" a) z

Listeleri oluşturmak ve değiştirmek için değiştirilebilir. Neredeyse tüm veri yapıları işlevsel eşdeğerlerine kolayca dönüştürülebilir. Burada yığınların, kuyrukların, sol yığınların, kırmızı-siyah ağaçların, tembel listelerin değişmez uygulamalarını sağlayan bir sayfa yazdım . Tek bir kod snippet'i değiştirilemez durum içermiyor. Bir ağacı "mutasyona uğratmak" için, istediğim yeni düğüme sahip yepyeni bir tane oluşturuyorum - bu çok verimli çünkü ağaçtaki her düğümün bir kopyasını yapmam gerekmiyor, eskileri yeni ağacı.

Daha önemli bir örnek kullanarak, ben de tamamen vatansız olan bu SQL ayrıştırıcı yazdı (ya da en azından benim kod vatansız, temel lexing kütüphane vatansız olup olmadığını bilmiyorum).

Vatansız programlama, durumsal programlama kadar etkileyici ve güçlüdür, vatansız düşünmeye başlamak için kendinizi eğitmek için biraz pratik gerektirir. Tabii ki, "mümkünse durum bilgisi olmayan programlama, gerektiğinde durum bilgisi olan programlama" en saf olmayan fonksiyonel dillerin sloganı gibi görünmektedir. İşlevsel yaklaşım o kadar da temiz ya da verimli olmadığında mabula geri düşmenin bir zararı yoktur.


7
Pacman örneğini seviyorum. Ancak bu sadece bir problemi diğerini yükseltmek için çözebilir: Ya başka bir şey mevcut Pacman nesnesine bir referans içeriyorsa? O zaman çöp toplanıp değiştirilmez; bunun yerine nesnenin biri geçersiz olan iki kopyası elde edersiniz. Bu sorunu nasıl ele alıyorsunuz?
Mason Wheeler

9
Açıkçası, yeni Pacman nesnesiyle yeni bir "başka şey" yaratmanız gerekiyor;) Tabii ki, bu rotayı çok ileri götürürsek, her şey değiştiğinde tüm dünyamız için nesne grafiğini yeniden yaratırız. Burada daha iyi bir yaklaşım açıklanmaktadır ( prog21.dadgum.com/26.html ): nesnelerin kendilerini ve tüm bağımlılıklarını güncellemelerini sağlamak yerine, durumları hakkında iletileri tüm olayları işleyen bir olay döngüsüne iletmelerini sağlamak çok daha kolaydır. güncellenmesi. Bu, grafikteki hangi nesnelerin güncellenmesi gerektiğine ve hangilerinin güncellenmemesine karar vermeyi çok daha kolay hale getirir.
Juliet

6
@Juliet, bir şüphem var - tamamen zorunluluk zihniyetimde, özyineleme bir noktada bitmeli, aksi takdirde sonunda bir yığın taşması üreteceksiniz. Özyinelemeli pacman örneğinde, yığın nasıl tutulur - nesne işlevin başlangıcında dolaylı olarak atlatılır mı?
BlueStrat

9
@BlueStrat - iyi soru ... eğer bir "kuyruk çağrısı" ise ... yani özyinelemeli çağrı fonksiyondaki son şeydir ... o zaman sistemin yeni bir yığın çerçevesi oluşturması gerekmez ... sadece bir öncekini tekrar kullan. Bu, işlevsel programlama dilleri için ortak bir optimizasyondur. en.wikipedia.org/wiki/Tail_call
reteptilian

4
@MichaelOsofsky, veritabanları ve API'lerle etkileşim kurarken, her zaman iletişim kurabilecekleri bir 'dış dünya' vardır. Bu durumda,% 100 işlevselliğe gidemezsiniz. Dış dünyaya sadece bir giriş ve bir çıkış olması için bu 'işlevsel olmayan' kodun yalıtılmış ve soyutlanmış halde tutulması önemlidir. Bu şekilde, kodunuzun geri kalanını çalışır durumda tutabilirsiniz.
Chielt

76

Kısa cevap: yapamazsınız.

Öyleyse değişmezlik konusundaki yaygara nedir?

Zorunlu bir dilde bilgiliyseniz, "küresellerin kötü" olduğunu bilirsiniz. Neden? Çünkü kodunuzda çözülmesi çok zor bazı bağımlılıkları ortaya çıkarır (veya potansiyel olarak tanıtırlar). Ve bağımlılıklar iyi değil; kodunuzun modüler olmasını istiyorsunuz . Programın bölümleri diğer bölümleri mümkün olduğunca az etkilemez. Ve FP modülerlik Holy Grail getiren: hiçbir yan etkisi hiç . Sadece f (x) = y değerine sahipsiniz. X'i yerleştirin, y'yi çıkarın. X veya başka bir şeyde değişiklik yok. FP, devlet hakkında düşünmeyi bırakıp değerler açısından düşünmeye başlar. Tüm işlevleriniz sadece değerleri alır ve yeni değerler üretir.

Bunun birkaç avantajı vardır.

Öncelikle, hiçbir yan etkisi daha basit programlar anlamına gelir, akıl yürütmesi daha kolaydır. Programın yeni bir parçasının kullanılmasının mevcut, çalışan bir parçayı etkileyip çökeceğinden endişe etmeyin.

İkincisi, bu programı önemsiz bir şekilde paralelleştirilebilir kılar (verimli paralelleştirme başka bir konudur).

Üçüncüsü, bazı olası performans avantajları vardır. Diyelim ki bir fonksiyonunuz var:

double x = 2 * x

Şimdi 3 in değerini koyuyorsunuz ve 6 değerini alıyorsunuz. Her zaman. Ama bunu zorunlu olarak da yapabilirsiniz, değil mi? Evet. Fakat sorun şudur ki, zorunlu olarak daha fazlasını da yapabilirsiniz . Yapabilirim:

int y = 2;
int double(x){ return x * y; }

ama ben de yapabilirim

int y = 2;
int double(x){ return x * (y++); }

Zorunlu derleyici, yan etkilere sahip olup olmadığımı bilmiyor, bu da optimize etmeyi daha zor hale getiriyor (yani çift 2'nin her seferinde 4 olması gerekmiyor). Fonksiyonel olan benim yapmayacağımı biliyor - bu nedenle, "çift 2" her gördüğünde optimize edebilir.

Şimdi, her seferinde yeni değerler oluşturmak, bilgisayar belleği açısından karmaşık değer türleri için inanılmaz derecede israf gibi görünse de, böyle olmak zorunda değildir. Çünkü f (x) = y değerine sahipseniz ve x ve y değerleri "çoğunlukla aynı" ise (örneğin, yalnızca birkaç yaprakta farklılık gösteren ağaçlar), x ve y belleğin parçalarını paylaşabilir - çünkü ikisi de mutasyona uğramaz .

Öyleyse bu değişmez şey çok büyükse, değişebilir durum olmadan faydalı bir şey yapamayacağınıza neden cevap verdim. Değişkenlik olmadan, tüm programınız dev bir f (x) = y işlevi olacaktır. Aynı şey programınızın tüm bölümleri için de geçerlidir: sadece işlevler ve "saf" anlamda işlevler. Dediğim gibi, bu her seferinde f (x) = y anlamına gelir . Bu nedenle, örneğin readFile ("myFile.txt") öğesinin her seferinde aynı dize değerini döndürmesi gerekir. Çok faydalı değil.

Bu nedenle, her FP bazı mutasyon durumu sağlar. "Saf" işlevsel diller (örn. Haskell) bunu monadlar gibi biraz korkutucu kavramlar kullanarak yaparken, "saf olmayan" diller (örneğin ML) buna doğrudan izin verir.

Ve elbette, fonksiyonel diller, birinci sınıf fonksiyonlar gibi programlamayı daha verimli hale getiren bir dizi başka güzellikle birlikte gelir.


2
<< readFile ("myFile.txt") öğesinin her seferinde aynı dize değerini döndürmesi gerekir. Çok yararlı değil. >> Sanırım küresel olanı sakladığınız sürece bir dosya sistemi. Bunu ikinci bir parametre olarak görürseniz ve diğer süreçlerin dosya sistemi2 = write (filesystem1, fd, pos, "string") ile her değiştirdiklerinde dosya sistemine yeni bir başvuru döndürmesine izin verir ve tüm işlemlerin dosya sistemine referanslarını değiştirmesine izin verirseniz , işletim sisteminin daha net bir resmini elde edebiliriz.
eel ghEEz

@eelghEEz, Datomic'in veritabanlarına uyguladığı yaklaşım budur.
Jason

1
Paradigmalar arasındaki açık ve öz karşılaştırma için +1. Bir öneri, int double(x){ return x * (++y); }hala ++y
bilinmeyen

@eelghEEz Bir alternatif olduğundan emin değilim, gerçekten başka biri var mı? Bir (saf-) FP bağlamına bilgi vermek için, "bir ölçüm alırsınız", örneğin "zaman damgasında X, sıcaklık Y'dir". Birisi sıcaklık isterse, örtük olarak X = anlamına gelebilir, ancak sıcaklığı evrensel bir zaman fonksiyonu olarak isteyemezler, değil mi? FP değişmez bir durumla uğraşır ve iç ve dış kaynaklardan değişebilir bir durumdan değişmez bir durum yaratmanız gerekir . Endeksler, zaman damgaları, vb. Faydalıdır ancak değişebilirliğe diktir - VCS gibi sürüm kontrolünün kendisi gibi.
John P

29

İşlevsel programlamanın 'durumu' olmadığını söylemenin biraz yanıltıcı olduğunu ve karışıklığın nedeni olabileceğini unutmayın. Kesinlikle 'değişebilir durumu' yoktur, ancak yine de manipüle edilmiş değerlere sahip olabilir; bunlar yerinde değiştirilemez (örneğin eski değerlerden yeni değerler oluşturmanız gerekir).

Bu kaba bir aşırı basitleştirme, ancak sınıflardaki tüm özelliklerin yalnızca yapıcıda bir kez ayarlandığı bir OO diline sahip olduğunuzu düşünün, tüm yöntemler statik işlevlerdir. Yöntemlerin, hesaplamaları için ihtiyaç duydukları tüm değerleri içeren nesneleri almasını ve ardından sonuçla birlikte yeni nesneleri döndürmesini (belki de aynı nesnenin yeni bir örneğini) sağlayarak hemen hemen her türlü hesaplamayı yapabilirsiniz.

Mevcut kodu bu paradigmaya çevirmek 'zor' olabilir, ancak bunun nedeni, kod hakkında tamamen farklı bir düşünme yöntemi gerektirmesidir. Yan etki olarak çoğu durumda paralellik için ücretsiz olarak çok fazla fırsat elde edersiniz.

Zeyilname: (Değişmesi gereken değerleri nasıl takip edeceğinize ilişkin düzenlemenizle ilgili olarak)
Elbette değişmez bir veri yapısında saklanırlar ...

Bu önerilen bir 'çözüm' değildir, ancak bunun her zaman işe yarayacağını görmenin en kolay yolu, bu değişmez değerleri 'değişken adı' ile anahtarlanmış bir haritaya (sözlük / karma) benzer bir yapıda saklayabilmenizdir.

Açıkçası pratik çözümlerde daha aklı başında bir yaklaşım kullanacaksınız, ancak bu en kötü durumun, başka hiçbir şey işe yaramazsa, invokasyon ağacınız boyunca taşıdığınız böyle bir harita ile değişebilir durumu 'simüle edebileceğinizi' gösterir.


2
Tamam, başlığı değiştirdim. Ancak cevabınız daha da kötü bir soruna yol açıyor gibi görünüyor. Durumunda bir şey her değiştiğinde her nesneyi yeniden oluşturmak zorunda kalırsam, tüm CPU zamanımı nesneleri inşa etmekten başka bir şey yapmadan geçireceğim. Burada, bir kerede ekranda (ve ekran dışında) hareket eden, birbirleriyle etkileşime girmesi gereken birçok şeyin olduğu oyun programlamasını düşünüyorum. Tüm motorun belirli bir kademesi vardır: Yapacağınız her şey, X milisaniye sayısı ile yapmanız gerekir. Elbette tüm nesneleri sürekli olarak geri dönüştürmekten daha iyi bir yol var mı?
Mason Wheeler

4
Bunun güzelliği, değişmezliğin uygulamada değil, dilde olmasıdır. Birkaç hileyle, uygulama aslında durumu değiştirirken dilde değişken bir duruma sahip olabilirsiniz. Bkz. Örneğin Haskell'in ST monad.
CesarB

4
@Mason: Derleyici, durumu yerinde değiştirmenin sizin yapabileceğinizden (güvenli) nerede (iş parçacığı) güvenli olduğuna karar verebileceğinden çok daha iyi bir karar verebilir.
jerryjvl

Oyunlar için hızın önemli olmadığı herhangi bir parça için değişmez kaçınmanız gerektiğini düşünüyorum. Değişmez bir dil sizin için optimize edilebilir, ancak hiçbir şey CPU'ların hızlı yaptığı belleği değiştirmekten daha hızlı olamaz. Ve eğer ortaya çıkarsa, zorunlu olan 10 veya 20 yer varsa, oyun menüleri gibi çok ayrı alanlar için modüler hale getiremezseniz, tamamen değişmez bir şekilde kaçınmanız gerektiğini düşünüyorum. Ve özellikle oyun mantığı değişmez kullanmak için güzel bir yer olabilir, çünkü iş kuralları gibi saf sistemlerin karmaşık modellemesini yapmak için harika olduğunu düşünüyorum.
LegendLength

@LegendLength kendinle çelişiyorsun.
Ixx

18

Bence hafif bir yanlış anlama var. Saf fonksiyonel programların durumu vardır. Fark, bu durumun modellenmesidir. Saf fonksiyonel programlamada durum, bazı durumları alıp bir sonraki duruma geri dönen fonksiyonlar tarafından manipüle edilir. Durumlar boyunca sıralama, daha sonra durumun bir saf fonksiyonlar dizisinden geçirilmesiyle elde edilir.

Küresel değişebilir durum bile bu şekilde modellenebilir. Örneğin Haskell'de bir program Dünya'dan Dünyaya bir fonksiyondur. Yani, tüm evreni geçersiniz ve program yeni bir evren döndürür. Ancak pratikte, evrenin yalnızca programınızın gerçekten ilgilendiği kısımlarını geçmeniz gerekir. Ve programlar aslında , programın çalıştığı işletim ortamı için talimatlar görevi gören bir dizi eylem döndürür .

Bunun zorunlu programlama açısından açıklanmasını görmek istediniz. Tamam, işlevsel bir dilde gerçekten basit bazı zorunlu programlara bakalım.

Bu kodu düşünün:

int x = 1;
int y = x + 1;
x = x + y;
return x;

Oldukça bataklık standart zorunlu kod. İlginç bir şey yapmaz, ama gösterim için sorun değil. Sanırım burada bir devlet var. X değişkeninin değeri zamanla değişir. Şimdi yeni bir sözdizimi icat ederek gösterimi biraz değiştirelim:

let x = 1 in
let y = x + 1 in
let z = x + y in z 

Bunun ne anlama geldiğini netleştirmek için parantez koyun:

let x = 1 in (let y = x + 1 in (let z = x + y in (z)))

Gördüğünüz gibi, durum aşağıdaki ifadelerin serbest değişkenlerini bağlayan bir dizi saf ifade ile modellenmiştir.

Bu modelin her türlü durumu, hatta ES'yi modelleyebileceğini göreceksiniz.


Bu bir Monad gibi mi?
CMCDragonkai

Şunu düşünür müsünüz: A seviye 1'de beyan edicidir B seviye 2'de beyan edicidir, A'nın zorunlu olduğunu düşünür. C seviye 3'te deklaratiftir, B'nin zorunlu olduğunu düşünür. Soyutlama katmanını artırdıkça, soyutlama katmanındaki düşük dillerin her zaman kendisinden daha zorunlu olduğunu düşünür.
CMCDragonkai

14

İşte sen değişken devlet olmadan kod yazmak nasıl : yerine değişken değişkenlere değişen devlet koyma, sen fonksiyonların parametreleri koydu. Ve döngüler yazmak yerine, yinelemeli işlevler yazarsınız. Örneğin bu zorunlu kod:

f_imperative(y) {
  local x;
  x := e;
  while p(x, y) do
    x := g(x, y)
  return h(x, y)
}

bu işlevsel kod olur (Şema benzeri sözdizimi):

(define (f-functional y) 
  (letrec (
     (f-helper (lambda (x y)
                  (if (p x y) 
                     (f-helper (g x y) y)
                     (h x y)))))
     (f-helper e y)))

veya bu Haskellish kodu

f_fun y = h x_final y
   where x_initial = e
         x_final   = loop x_initial
         loop x = if p x y then loop (g x y) else x

Gelince neden fonksiyonel programcılar (sormadınız olan) bu yapmak ister, programınızın daha parçaları, vatansız olan yolu daha şey mola kalmadan parçaları bir araya getirmede vardır . Vatansız paradigmanın gücü, vatansızlıkta (veya saflıkta) değil , güçlü, yeniden kullanılabilir işlevler yazmanıza ve bunları birleştirmenize izin verir.

John Hughes'un Neden İşlevsel Programlama Önemlidir adlı makalesinde birçok örnek içeren iyi bir öğretici bulabilirsiniz .


13

Aynı şeyi yapmanın sadece farklı yolları.

3, 5 ve 10 sayılarını eklemek gibi basit bir örnek düşünün. Bunu, önce 5 değerini ekleyerek 3 değerini değiştirip, o "3" e 10 ekleyerek ve ardından " 3 "(18). Bu açıkça gülünç görünüyor, ancak özünde devlet temelli zorunlu programlama genellikle yapılır. Gerçekten, 3 değerine sahip, ancak farklı birçok farklı "3" ler olabilir. Tüm bunlar tuhaf görünüyor, çünkü sayıların değişmez olduğu oldukça muazzam derecede mantıklı bir fikirle o kadar kökleşmiş olduk.

Şimdi değerleri değiştirilemez olarak alırken 3, 5 ve 10 eklemeyi düşünün. Başka bir değer, 8 üretmek için 3 ve 5 ekledikten sonra başka bir değer, 18 üretmek için bu değere 10 eklersiniz.

Bunlar aynı şeyi yapmanın eşdeğer yollarıdır. Gerekli tüm bilgiler her iki yöntemde de vardır, ancak farklı şekillerde. Birinde bilgi durum olarak ve durum değiştirme kurallarında bulunur. Diğerinde bilgi değişmez veriler ve fonksiyonel tanımlarda mevcuttur.


10

Tartışmaya geç kaldım, ancak fonksiyonel programlama ile mücadele eden insanlar için birkaç nokta eklemek istedim.

  1. İşlevsel diller, zorunlu dillerle aynı durum güncelleştirmelerini korur, ancak bunu güncelleştirilmiş durumu sonraki işlev çağrılarına geçirerek yapar . İşte bir sayı hattında seyahat etmenin çok basit bir örneği. Eyaletiniz geçerli konumunuzdur.

İlk olarak zorunlu yol (sözde kodda)

moveTo(dest, cur):
    while (cur != dest):
         if (cur < dest):
             cur += 1
         else:
             cur -= 1
    return cur

Şimdi işlevsel yol (sözde kodda). Üçlü operatöre yaslanmışım çünkü zorunlu arka plandaki insanların aslında bu kodu okuyabilmesini istiyorum. Üçlü operatörü çok fazla kullanmıyorsanız (zorunlu günlerimden her zaman kaçındım) işte böyle çalışır.

predicate ? if-true-expression : if-false-expression

Üçlü ifadeyi, yanlış ifadenin yerine yeni bir üçlü ifadeyi yerleştirerek zincirleyebilirsiniz.

predicate1 ? if-true1-expression :
predicate2 ? if-true2-expression :
else-expression

Bunu göz önünde bulundurarak, burada fonksiyonel sürüm.

moveTo(dest, cur):
    return (
        cur == dest ? return cur :
        cur < dest ? moveTo(dest, cur + 1) : 
        moveTo(dest, cur - 1)
    )

Bu önemsiz bir örnek. Bu, bir oyun dünyasındaki insanları hareket ettiriyor olsaydı, nesnenin ekranda mevcut konumunu çizmek ve nesnenin ne kadar hızlı hareket ettiğine bağlı olarak her çağrıda biraz gecikme yapmak gibi yan etkiler tanıtmanız gerekirdi. Ama hala değişebilir bir duruma ihtiyacınız olmazdı.

  1. Ders fonksiyonel dillerin fonksiyonu farklı parametrelerle çağırarak "mutasyona uğraması" dır. Açıkçası bu, herhangi bir değişkeni gerçekten değiştirmez, ancak benzer bir etki elde edersiniz. Bu, fonksiyonel programlama yapmak istiyorsanız, yinelemeli düşünmeye alışmanız gerektiği anlamına gelir.

  2. Özyineli düşünmeyi öğrenmek zor değildir, ancak hem pratik hem de bir araç seti gerektirir. Faktöriyel hesaplamak için özyineleme kullandıkları "Java'yı Öğrenin" kitabındaki bu küçük bölüm onu ​​kesmez. Yinelemeden yinelemeli işlemler yapmak (bu nedenle kuyruk özyinelemesinin işlevsel dil için esastır), devamlar, değişmezler, vb. Gibi bir araç setine ihtiyacınız vardır. fonksiyonel programlama için.

Benim tavsiyem Küçük Schemer yapmak ("okumak" değil "yapmak" dediğimi unutmayın) ve daha sonra SICP tüm egzersizleri yapmaktır. İşiniz bittiğinde, başladığınızdan farklı bir beyniniz olacak.


8

Aslında, değişken durumu olmayan dillerde bile değişebilir duruma benzeyen bir şeye sahip olmak oldukça kolaydır.

Türünde bir işlev düşünün s -> (a, s). Haskell sözdiziminden çeviri, " s" türünde bir parametre alan ve " a" ve " s" türlerinde bir çift değer döndüren bir işlev anlamına gelir . Durumumuz stürü ise, bu işlev bir durumu alır ve yeni bir durum ve muhtemelen bir değer döndürür (her zaman "birim" aka (), voidC / C ++ 'da " a" ) kullanımı önerilir. Bunun gibi türlerle birkaç işlev çağrısını zincirlendirirseniz (durumu bir işlevden döndürüp diğerine geçirirseniz), "değişebilir" duruma sahipsiniz (aslında her işlevde yeni bir durum oluşturup eskisini terk edersiniz) ).

Değişken durumu programınızın yürüttüğü "alan" olarak hayal edip etmediğinizi anlamak ve daha sonra zaman boyutunu düşünmek daha kolay olabilir. T1 anında, "boşluk" belirli bir durumdadır (örneğin, bazı bellek konumlarının değeri 5'dir). Daha sonraki bir anlık t2'de, farklı bir durumdadır (örneğin, bellek konumunun artık 10 değeri vardır). Bu zaman dilimlerinin her biri bir durumdur ve değişmezdir (bunları değiştirmek için zamanda geriye gidemezsiniz). Böylece, bu bakış açısından, bir zaman okuyla (değişebilir durumunuz) tam uzay zamanından bir dizi uzay zaman dilimine (birkaç değişmez durum) geçtiniz ve programınız her dilime bir değer olarak davranıyor ve her birini hesaplıyor bir öncekine uygulanan bir işlev olarak.

Tamam, belki bu anlaşılması kolay değildi :-)

Tüm program durumunu, yalnızca bir sonraki anı (yeni bir tane oluşturulduktan hemen sonra) atılmak üzere yaratılması gereken bir değer olarak açıkça temsil etmek yetersiz görünebilir. Bazı algoritmalar için doğal olabilir, ancak olmadığında başka bir hile vardır. Gerçek bir durum yerine, bir işaretleyiciden başka bir şey olmayan sahte bir durum kullanabilirsiniz (bu sahte durumun türünü diyelim State#). Bu sahte durum dilin bakış açısından vardır ve diğer herhangi bir değer gibi geçirilir, ancak derleyici makine kodunu oluştururken onu tamamen atlar. Yalnızca yürütme sırasını işaretlemeye yarar.

Örnek olarak, derleyicinin bize aşağıdaki işlevleri verdiğini varsayalım:

readRef :: Ref a -> State# -> (a, State#)
writeRef :: Ref a -> a -> State# -> (a, State#)

Bu Haskell benzeri bildirimlerden çeviri, readRef" a" türünde bir değere ve sahte duruma bir işaretçi veya tanıtıcıya benzeyen bir şey alır ave ilk parametre ve yeni bir sahte durum tarafından işaret edilen " " türünün değerini döndürür . writeRefbenzerdir, ancak bunun yerine gösterilen değeri değiştirir.

Eğer çağırır readRefve sonra geri döndürülen sahte durumu writeRef(belki ortadaki ilgisiz işlevlere yapılan diğer çağrılarla; bu durum değerleri bir işlev çağrıları "zinciri" oluşturur), yazılan değeri döndürür. writeRefAynı işaretçi / tanıtıcıyla tekrar çağırabilirsiniz ve aynı bellek konumuna yazacaktır - ancak kavramsal olarak yeni (sahte) bir durum döndürdüğü için, (sahte) durum hala kararsızdır (yeni bir tane oluşturulmuştur " "). Derleyici, hesaplanması gereken gerçek durum değişkeni varsa işlevleri çağırmak zorunda oldukları sırayla çağıracaktır, ancak var olan tek durum gerçek donanımın tam (değiştirilebilir) durumudur.

(Haskell bilenler bakmak, isteyenler daha fazla ayrıntı görmek için. Ben birkaç önemli ayrıntı şeyler çok basitleştirilmiş ve ommited göreceksiniz Control.Monad.Stategelen mtlve en ST sve IO(aka ST RealWorld) monads.)

Neden böyle dolambaçlı bir şekilde yapıldığını merak edebilirsiniz (sadece dilde değişebilir bir duruma sahip olmak yerine). Asıl avantaj, programınızın durumunu yeniden yapılandırmış olmanızdır . Önceden örtük olan (program durumunuz küreseldi, uzaktan eylem gibi şeylere izin verme ) şimdi açık. Durumu almayan ve geri döndürmeyen işlevler durumu değiştiremez veya bundan etkilenemez; onlar "saf". Daha da iyisi, ayrı durum iş parçacıklarına sahip olabilirsiniz ve biraz tür bir büyü ile, saf olmayan bir safsa içine zorunlu bir hesaplamayı gömmek için kullanılabilirler ( STHaskell'deki monad, bu hile için normal olarak kullanılan olandır; State#yukarıda belirtildiği aslında GHC en içindedir State# suygulanışı kullandığı, STveIO monads).


7

Fonksiyonel programlama durumu önler ve vurgularişlevsellik. Hiçbir zaman devlet diye bir şey yoktur, ancak devlet aslında üzerinde çalıştığınız şeyin mimarisine değişmez veya pişmiş bir şey olabilir. Dosya sisteminden dosyaları yükleyen statik bir web sunucusu ile Rubik küpünü uygulayan bir program arasındaki farkı düşünün. Birincisi, bir isteği bir dosya yolu isteğine o dosyanın içeriğinden gelen bir yanıta dönüştürmek için tasarlanmış işlevler açısından uygulanacaktır. Küçük bir konfigürasyonun ötesinde neredeyse hiçbir duruma gerek yoktur (dosya sistemi 'durumu' gerçekten programın kapsamı dışındadır. Program, dosyaların hangi durumda olduğuna bakılmaksızın aynı şekilde çalışır). Ancak sonuncusunda, küpü ve program uygulamanızı bu küp üzerindeki işlemlerin durumunu nasıl değiştirdiğini modellemeniz gerekir.


Daha anti-fonksiyonel olduğumda, bir sabit disk gibi bir şey değiştirilebilir olduğunda nasıl iyi olabileceğini merak ettim. C # sınıflarımın tümü değiştirilebilir bir duruma sahipti ve mantıksal olarak bir sabit sürücüyü veya başka bir aygıtı taklit edebilirdi. İşlevsel olarak modeller ile modelledikleri gerçek makineler arasında bir uyumsuzluk vardı. İşlevsel olarak daha ayrıntılı bir şekilde inceledikten sonra, elde edeceğiniz faydaların bu sayıyı biraz daha ağır basabildiğini fark etmeye başladım. Ve bir kopyasını yapan bir sabit diski icat etmek fiziksel olarak mümkün olsaydı, aslında yararlı olurdu (dergi zaten zaten olduğu gibi).
LegendLength

5

Başkalarının verdiği harika cevaplara ek olarak, sınıfları Integerve StringJava'yı düşünün . Bu sınıfların örnekleri değiştirilemez, ancak bu sadece örneklerinin değiştirilememesi nedeniyle sınıfları işe yaramaz hale getirmez. Değişmezlik size biraz güvenlik sağlar. A anahtarı olarak bir String veya Integer örneği kullanırsanız Map, anahtar değiştirilemez. Bunu DateJava'daki sınıfla karşılaştırın :

Date date = new Date();
mymap.put(date, date.toString());
// Some time later:
date.setTime(new Date().getTime());

Haritanızda bir anahtarı sessizce değiştirdiniz! İşlevsel Programlama gibi değişmez nesnelerle çalışmak çok daha temizdir. Hangi yan etkilerin meydana geldiğini anlamak daha kolaydır - hiçbiri! Bu, programcı için daha kolay ve aynı zamanda optimize edici için daha kolay olduğu anlamına gelir.


2
Bunu anlıyorum, ama soruma cevap vermiyor. Bir bilgisayar programının gerçek dünyadaki bir olayın veya sürecin bir modeli olduğunu aklınızda tutarak, değerlerinizi değiştiremezseniz, değişen bir şeyi nasıl modellersiniz?
Mason Wheeler

Kesinlikle Integer ve String sınıflarıyla faydalı şeyler yapabilirsiniz. Değişmezlikleri değişebilir duruma sahip olamayacağınız anlamına gelmez.
Eddie

@Mason Wheeler - Bir şeyin ve onun durumunun iki farklı "şey" olduğunu anlayarak. Pacman'ın ne olduğu A zamanından B'ye değişmez. Pacman'ın nerede değiştiği. A zamanından B zamanına geçtiğinizde, aynı pacman, farklı durum olan pacman + durumunun yeni bir kombinasyonunu elde edersiniz. Durum değişmedi ... farklı durum.
RHSeeger

4

Oyunlar, Fonksiyonel Reaktif Programlama gibi yüksek etkileşimli uygulamalar için sizin dostunuzdur: Eğer oyununuzun dünyasının özelliklerini zamanla değişen değerler (ve / veya olay akışları) olarak formüle edebiliyorsanız , hazırsınız demektir! Bu formüller bazen bir durumu mutasyona uğratmaktan daha doğal ve kasıtlı olarak ortaya çıkacaktır, örneğin hareketli bir top için iyi bilinen x = v * t yasasını doğrudan kullanabilirsiniz . Ve daha neler, oyunun kuralları böyle bir yol yazılı oluşturma deneyimini daha iyi nesne yönelimli soyutlama daha. Örneğin, bu durumda, topun hızı, aynı zamanda topun çarpışmalarından oluşan olay akışına bağlı olan zamanla değişen bir değer olabilir. Daha somut tasarım konuları için bkz. Elm'de Oyunlar Yapma .


4

3

FORTRAN, COMMON bloğu olmadan çalışır: Geçtiğiniz değerlere ve yerel değişkenlere sahip yöntemler yazarsınız. Bu kadar.

Nesneye yönelik programlama bize durumu ve davranışı bir araya getirdi, ancak 1994'te C ++ ile ilk karşılaştığımda yeni bir fikirdi.

Tanrım, makine mühendisi olduğumda fonksiyonel bir programcıydım ve bunu bilmiyordum!


2
Bunun OO'ya sabitleyebileceğiniz bir şey olduğuna katılmıyorum. OO öncesi diller bağlantı durumunu ve algoritmaları teşvik etti. OO, onu yönetmek için daha iyi bir yol sağladı.
Jason Baker

"Cesaretlendirildi" - belki. OO onu dilin açık bir parçası haline getiriyor. C'de gizleme ve bilgi gizleme yapabilirsiniz, ancak OO dillerinin bunu çok daha kolay hale getirdiğini söyleyebilirim.
duffymo

2

Unutmayın: işlevsel diller Turing tamamlandı. Bu nedenle, belirsiz bir dilde yapacağınız herhangi bir yararlı görev işlevsel bir dilde yapılabilir. Yine de günün sonunda, melez bir yaklaşım hakkında söylenecek bir şey olduğunu düşünüyorum. F # ve Clojure gibi diller (ve eminim diğerleri) vatansız tasarımı teşvik eder, ancak gerektiğinde değişime izin verir.


İki dilin tamamlanması, aynı görevleri yerine getirebilecekleri anlamına gelmez. Bunun anlamı, aynı hesaplamayı yapabilmeleridir. Brainfuck Turing tamamlandı, ancak bir TCP yığını üzerinden iletişim kuramayacağından oldukça eminim.
RHSeeger

2
Elbette olabilir. Diyelim ki C ile aynı donanıma erişim göz önüne alındığında, bunu yapabilir. Bu pratik olacağı anlamına gelmez, ancak olasılık oradadır.
Jason Baker

2

Yararlı olan saf işlevsel bir dile sahip olamazsınız. Her zaman uğraşmanız gereken bir değişebilirlik seviyesi olacaktır, IO buna bir örnektir.

İşlevsel dilleri kullandığınız başka bir araç olarak düşünün. Bazı şeyler için iyi, ama diğerleri için değil. Verdiğiniz oyun örneği işlevsel bir dil kullanmanın en iyi yolu olmayabilir, en azından ekranda FP ile ilgili hiçbir şey yapamayacağınız değiştirilebilir bir durum olacaktır. Problemi düşünme biçiminiz ve FP ile çözdüğünüz problemlerin türü, zorunlu programlama ile alışkın olduğunuzdan farklı olacaktır.



-3

Bu çok basit. İşlevsel programlamada istediğiniz kadar çok değişken kullanabilirsiniz ... ancak yalnızca yerel değişkenlerse (işlevlerin içinde bulunur). Kodunuzu işlevlere sarın, değerleri bu işlevler arasında (geçirilen parametreler ve döndürülen değerler olarak) ileri geri geçirin ... ve hepsi bu kadar!

İşte bir örnek:

function ReadDataFromKeyboard() {
    $input_values = $_POST[];
    return $input_values;
}
function ProcessInformation($input_values) {
    if ($input_values['a'] > 10)
        return ($input_values['a'] + $input_values['b'] + 3);
    else if ($input_values['a'] > 5)
        return ($input_values['b'] * 3);
    else
        return ($input_values['b'] - $input_values['a'] - 7);
}
function DisplayToPage($data) {
    print "Based your input, the answer is: ";
    print $data;
    print "\n";
}

/* begin: */
DisplayToPage (
    ProcessInformation (
        GetDataFromKeyboard()
    )
);

John, bu hangi dil?
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.