Fonksiyonel programlamada değerleri hatırlamak


20

İşlevsel programlamayı öğrenme görevini üstlenmeye karar verdim. Şimdiye kadar bir patlama oldu ve ben 'ışığı' olduğu gibi gördüm. Ne yazık ki, soruları sektirebileceğim herhangi bir fonksiyonel programcı bilmiyorum. Stack Exchange ile tanışın.

Bir web / yazılım geliştirme kursu alıyorum, ancak eğitmenim fonksiyonel programlamaya aşina değil. Onu kullanmamda sorun yok ve sadece kodumu daha iyi okuyabilmek için nasıl çalıştığını anlamasına yardım etmemi istedi.

Bunu yapmanın en iyi yolunun, bir güce değer katmak gibi basit bir matematiksel işlevi göstererek olacağına karar verdim. Teorik olarak bunu önceden oluşturulmuş bir işlevle kolayca yapabilirim, ancak bu bir örneğin amacını bozabilir.

Her neyse, bir değeri nasıl tutacağımı anlamakta zorlanıyorum. Bu işlevsel programlama olduğundan değişkeni değiştiremiyorum. Bu zorunluluk kodlamak olsaydı, böyle bir şey olurdu:

(Aşağıdakilerin tümü sahte koddur)

f(x,y) {
  int z = x;
  for(int i = 0, i < y; i++){
    x = x * z;
  }
  return x;
}

Fonksiyonel programlamada emin değildim. Ben geldi budur:

f(x,y,z){
  if z == 'null',
    f(x,y,x);
  else if y > 1,
    f(x*z,y-1,z);
  else
    return x;
}

Bu doğru mu? Her ziki durumda da bir değer tutmam gerekiyor , ancak işlev programlamasında bunu nasıl yapacağımdan emin değildim. Teorik olarak, yaptığım gibi çalışır, ancak 'doğru' olup olmadığından emin değildim. Bunu yapmanın daha iyi bir yolu var mı?


32
Örneğinizin ciddiye alınmasını istiyorsanız, matematik probleminden ziyade pratik bir problemi çözmesini sağlayın. Geliştiriciler arasında "tüm FP iyi matematik problemlerini çözmektir" gibi bir klişe ve eğer örneğiniz Yet Another Mathematical Function ise, yaptığınız şeyi kullanışlı hale getirmek yerine sadece klişeyi güçlendiriyorsunuz.
Mason Wheeler

12
Gerçek dünyayı göz önünde bulundurarak girişiminiz aslında oldukça iyi. Tüm özyinelemeli çağrılarınız kuyruk çağrılarıdır , yani işlev onları çağırdıktan sonra başka bir şey yapmaz. Bu , onu destekleyen bir derleyici veya yorumlayıcının bunları en iyi duruma getirebileceği anlamına gelir, böylece özyinelemeli işleviniz orantılı bir miktar yerine sabit miktarda yığın bellek kullanır y.
8bittree

1
Destek için çok teşekkürler! Bu konuda hala çok yeniyim, bu yüzden sahte kodum mükemmel değil. @MasonWheeler Sanırım, bu durumda kodum gerçekten ciddiye alınmaz. Hala öğreniyorum ve FP'yi sevmemin nedeni Math-y olması. Örneğimin asıl amacı öğretmenime neden FP kullandığımı açıklamak. Ne olduğunu gerçekten anlamıyor, bu yüzden ona avantajlarını göstermenin iyi bir yolu gibi görünüyordu.
Ucenna

5
Kodu hangi dilde yazmayı planlıyorsunuz? Kullandığınız dile uygun olmayan bir stil kullanmaya çalışmayın.
Carsten S

Yanıtlar:


37

Her şeyden önce, "ışığı görmek" için tebrikler. Ufkunuzu genişleterek yazılım dünyasını daha iyi bir yer haline getirdiniz.

İkincisi, işlevsel programlamayı anlamayan bir profesörün "girinti kapalı" gibi trite yorumları dışında kodunuz hakkında faydalı bir şey söyleyemeyeceği hiçbir şekilde yoktur. Çoğu web geliştirme HTML / CSS / JavaScript kullanılarak yapıldığından, bu bir web geliştirme kursunda şaşırtıcı değildir. Aslında web geliştirme öğrenmeye ne kadar önem verdiğinize bağlı olarak, profesörünüzün öğrettiği araçları öğrenmek için çaba sarf etmek isteyebilirsiniz (olsa da acı verici - deneyimden biliyorum).

Belirtilen soruyu ele almak için: zorunlu kodunuz bir döngü kullanıyorsa, fonksiyonel kodunuz yinelemeli olacaktır.

(* raises x to the power of y *)
fun pow (x: real) (y: int) : real = 
    if y = 1 then x else x * (pow x (y-1))

Bu algoritmanın aslında zorunlu kodla aşağı yukarı aynı olduğunu unutmayın. Aslında, yukarıdaki döngü yinelemeli özyinelemeli işlemler için sözdizimsel şeker olarak düşünülebilir.

Bir yan not olarak, zaslında sizin zorunlu veya işlevsel kodunuzda bir değere gerek yoktur . Zorunlu işlevinizi şöyle yazmış olmalısınız:

def pow(x, y):
    var ret = 1
    for (i = 0; i < y; i++)
         ret = ret * x
    return ret

değişkenin anlamını değiştirmek yerine x.


Özyinelemeniz powpek doğru değil. Olduğu gibi , yerine pow 3 3döner . O olmalı8127else x * pow x (y-1).
8bittree

3
Hata! Doğru kodu yazmak zordur :) Düzeltildi ve ayrıca tür ek açıklamaları ekledim. @Ucenna SML olması gerekiyordu, ama bir süredir kullanmadım, bu yüzden sözdizimi biraz yanlış olabilir. Bir işlevi bildirmenin çok fazla lanet yolu var, doğru anahtar kelimeyi asla hatırlayamıyorum. Sözdizimi değişikliklerinin yanı sıra, kod JavaScript'te aynıdır.
gardenhead

2
@jwg Javascript'in bazı işlevsel yönleri vardır: işlevler iç içe geçmiş işlevleri tanımlayabilir, işlevleri döndürebilir ve işlevleri parametre olarak kabul edebilir; sözcüksel kapsam ile kapanmaları destekler (gerçi lisp dinamik kapsamı yoktur). Durum değiştirmekten ve verileri değiştirmekten kaçınmak programcının disiplinine bağlıdır.
Kasper van den Berg

1
@jwg "İşlevsel" dilin üzerinde mutabık kalınan bir tanımı yoktur (ne de "zorunlu", "nesne yönelimli" veya "beyan edici" için). Mümkün olduğunca bu terimleri kullanmaktan kaçınmaya çalışıyorum. Güneş altında dört temiz gruba ayrılacak çok fazla dil var.
gardenhead

1
Popülerlik korkunç bir metriktir, bu yüzden birisi dil veya araç X'in daha iyi olması gerektiğinden bahsettiğinde, bu kadar yaygın olarak kullanılır, çünkü argümanı sürdürmenin anlamsız olacağını biliyorum. ML dil ailesine şahsen Haskell'den daha aşinayım. Ama bunun doğru olup olmadığından da emin değilim; tahminimce geliştiricilerin büyük çoğunluğu Haskell'i ilk etapta denemedi .
gardenhead

33

Bu gerçekten gardenhead'in cevabına bir zeyilname, ama gördüğünüz desen için bir isim olduğunu belirtmek isterim: katlama.

Fonksiyonel programlamada, katlama , her işlem arasında bir değeri "hatırlayan" bir dizi değeri birleştirmenin bir yoludur. Zorunlu bir sayı listesi eklemeyi düşünün:

def sum_all(xs):
  total = 0
  for x in xs:
    total = total + x
  return total

Biz değerler listesi almak xsve bir başlangıç durumunu ait 0(temsil totalbu durumda). Daha sonra, her bir xgiriş için xs, bu değeri birleştirme işlemine (bu durumda ekleme) göre mevcut durumla birleştiririz ve sonucu yeni durum olarak kullanırız. Aslında, sum_all([1, 2, 3])eşdeğerdir (3 + (2 + (1 + 0))). Bu desen, işlevleri bağımsız değişken olarak kabul eden bir işlev olan daha yüksek bir sıra işlevine çıkarılabilir :

def fold(items, initial_state, combiner_func):
  state = initial_state
  for item in items:
    state = combiner_func(item, state)
  return state

def sum_all(xs):
  return fold(xs, 0, lambda x y: x + y)

Bu uygulama foldhala zorunludur, ancak yinelemeli olarak da yapılabilir:

def fold_recursive(items, initial_state, combiner_func):
  if not is_empty(items):
    state = combiner_func(initial_state, first_item(items))
    return fold_recursive(rest_items(items), state, combiner_func)
  else:
    return initial_state

Bir katlama olarak ifade edilen işleviniz basitçe:

def exponent(base, power):
  return fold(repeat(base, power), 1, lambda x y: x * y))

... burada repeat(x, n)bir nkopya listesi döndürür x.

Birçok işlev, özellikle de işlevsel programlamaya yönelik olanlar, standart kitaplıklarında katlama olanağı sunar. Javascript bile adı altında sağlar reduce. Genel olarak, bir döngü içinde bir değeri "hatırlamak" için özyineleme kullandığınızı fark ederseniz, muhtemelen bir kat istersiniz.


8
Bir sorunun bir kıvrım veya harita ile ne zaman çözülebileceğini kesinlikle öğrenin. FP'de neredeyse tüm döngüler katlama veya harita olarak ifade edilebilir; bu nedenle açık özyineleme genellikle gerekli değildir.
Carcigenicate

1
Bazı dillerde, sadece yazabilirsinizfold(repeat(base, power), 1, *)
user253751

4
Rico Kahler: scanaslında folddeğerler listesini tek bir değerde birleştirmek yerine birleştirilir ve her bir ara değer yol boyunca geri tükürülür , sadece kat oluşturmak için katın oluşturduğu tüm ara durumların bir listesini üretir. son durum. fold(Her döngü işlemi) açısından uygulanabilir .
Jack

4
@RicoKahler Ve anlayabildiğim kadarıyla, indirimler ve kıvrımlar aynı şey. Haskell "katlama" terimini kullanırken Clojure "azaltma" yı tercih eder. Davranışları benim için aynı görünüyor.
16:20

2
@Ucenna: Hem değişken hem de işlevdir. İşlevsel programlamada, işlevler sayılar ve dizeler gibi değerlerdir - bunları değişkenlerde saklayabilir, bunları diğer işlevlere argüman olarak aktarabilir, işlevlerden geri döndürebilir ve genellikle diğer değerler gibi davranabilirsiniz. Yani combiner_funcbir argüman ve iki öğeyi nasıl birleştirmek istediğini tanımlayan anonim bir işlevsum_all geçiyor (bu biraz - isimlendirmeden bir işlev değeri yaratıyor). lambda
Jack

8

Bu, haritaları ve kıvrımları açıklamaya yardımcı olacak ek bir cevaptır. Aşağıdaki örnekler için bu listeyi kullanacağım. Unutmayın, bu liste değişmez, bu yüzden asla değişmeyecek:

var numbers = [1, 2, 3, 4, 5]

Örneklerimde sayılar kullanacağım çünkü okunması kolay kodlara yol açıyorlar. Bununla birlikte, kıvrımların geleneksel bir zorunluluk döngüsünün kullanılabileceği her şey için kullanılabileceğini unutmayın.

Bir harita şeyin bir listesini ve bir işlev alır ve işlevini kullanarak modifiye edilmiş bir listesini döndürür. Her öğe işleve iletilir ve işlevin döndürdüğü her şey olur.

Bunun en kolay örneği, listedeki her sayıya bir sayı eklemektir. Dili agnostik hale getirmek için sözde kod kullanacağım:

function add-two(n):
    return n + 2

var numbers2 =
    map(add-two, numbers) 

Yazdırdıysanız numbers2, [3, 4, 5, 6, 7]her bir öğeye 2 eklenmiş ilk listenin hangisi olduğunu görürsünüz . Fonksiyonun kullanımına add-twoverildiğine dikkat edin map.

Kapaklar benzerdir, ancak onlara vermeniz gereken işlev 2 bağımsız değişken almalıdır. İlk argüman genellikle akümülatördür (en yaygın olan sol katta). Akümülatör, döngü sırasında geçirilen verilerdir. İkinci argüman listenin geçerli öğesidir; tıpkı mapfonksiyon için yukarıdaki gibi .

function add-together(n1, n2):
    return n1 + n2

var sum =
    fold(add-together, 0, numbers)

Yazdırdıysanız sum, sayı listesinin toplamını görürsünüz: 15.

İşte argümanların yapması foldgerekenler:

  1. Bu, verdiğimiz işlevdir. Kapak, geçerli akümülatör işlevini ve listenin geçerli öğesini geçirir. Fonksiyonun geri dönüşü ne olursa olsun, bir sonraki sefer fonksiyona aktarılacak olan yeni akümülatör haline gelecektir. FP stilini döngüye alırken değerleri bu şekilde "hatırlarsınız". 2 sayı alan ve ekleyen bir fonksiyon verdim.

  2. Bu ilk akümülatör; Akümülatörün listedeki herhangi bir öğe işlenmeden önceki gibi başlar. Sayıları toplarken, birlikte sayı eklemeden önceki toplam nedir? 0, ikinci argüman olarak geçtim.

  3. Son olarak, haritada olduğu gibi, işlenmesi için sayı listesini de iletiyoruz.

Kıvrımlar hala mantıklı değilse, bunu düşünün. Yazarken:

# Notice I passed the plus operator directly this time, 
#  instead of wrapping it in another function. 
fold(+, 0, numbers)

Temel olarak, geçirilen işlevi listedeki her öğe arasına koyarsınız ve ilk akümülatörü sola veya sağa eklersiniz (sol veya sağ kat olmasına bağlı olarak), bu nedenle:

[1, 2, 3, 4, 5]

Oluyor:

0 + 1 + 2 + 3 + 4 + 5
^ Note the initial accumulator being added onto the left (for a left fold).

Bu 15'e eşit.

mapBir listeyi aynı uzunlukta başka bir listeye dönüştürmek istediğinizde a kullanın .

foldBir listeyi, sayı listesinin toplanması gibi tek bir değere dönüştürmek istediğinizde a kullanın .

@Jorg'un yorumlarda belirttiği gibi, "tek değer" bir sayı gibi basit bir şey olmak zorunda değildir; bir liste veya bir demet de dahil olmak üzere herhangi bir nesne olabilir! Benim için kıvrımları tıklatmamın yolu, kıvrım açısından bir harita tanımlamaktı . Akümülatörün nasıl bir liste olduğuna dikkat edin:

function map(f, list):
    fold(
        function(xs, x): # xs is the list that has been processed so far
            xs.add( f(x) ) # Add returns the list instead of mutating it
        , [] # Before any of the list has been processed, we have an empty list
        , list) 

Dürüst olmak gerekirse, her birini anladıktan sonra, neredeyse tüm döngülerin bir kat veya harita ile değiştirilebileceğini fark edeceksiniz.


1
@Ucenna @Ucenna Kodunuzda birkaç hata var ( iasla tanımlanmadığı gibi ), ama bence doğru fikre sahipsiniz. Örneğinizle ilgili bir sorun: ( x) işlevine , listenin tamamını değil, bir kerede yalnızca bir öğe iletilir. İlk kez xçağrıldığında, yilk argüman olarak ilk akümülatörünüzden ( ) ve ikinci argüman olarak ilk öğeden geçer. Bir sonraki çalıştırıldığında, xsoldaki yeni akümülatöre ( xilk kez dönen ne olursa olsun ) ve ikinci argüman olarak listenin ikinci öğesine geçirilecek.
Carcigenicate

1
@Ucenna Artık temel fikre sahip olduğunuza göre, Jack'in uygulamasına tekrar bakın.
Carcigenicate

1
@Ucenna: Katlama için verilen fonksiyonun, akümülatörü maalesef birinci veya ikinci argüman olarak alıp almadığı farklı langların farklı tercihlerine sahiptir. Kıvrımları öğretmek için ekleme gibi değişmeli bir işlem kullanmanın güzel nedenlerinden biri.
Jack

3
" foldBir listeyi tek bir değere dönüştürmek istediğinizde (bir sayı listesini toplamak gibi) kullanın." - Ben sadece bu "tek bir değer" keyfi bir liste olabilir unutmayın ... bir liste dahil! Aslında, foldgenel bir yineleme yöntemidir, yinelemenin yapabileceği her şeyi yapabilir. Örneğin mapönemsiz bir şekilde func map(f, l) = fold((xs, x) => append(xs, f(x)), [], l)Burada ifade edilebilir , tarafından hesaplanan "tek değer" foldaslında bir listedir.
Jörg W Mittag

2
… Muhtemelen bir listeyle ilgili yapmak istiyorum, ile yapılabilir fold. Bir liste olmak zorunda değil, boş / boş olarak ifade edilebilen her koleksiyon yapılacak. Bu temelde herhangi bir yineleyicinin yapacağı anlamına gelir. (sanırım "katamorfizm" kelimesini atmak acemi bir giriş için çok fazla olurdu, ama :-D)
Jörg W Mittag

1

Yapı işlevselliğiyle çözülemeyen iyi problemleri bulmak zor. Ve eğer yerleşikse, o zaman x dilinde iyi bir stil örneği olarak kullanılmalıdır.

Örneğin haskell'de (^)Prelude'da zaten bir işleve sahipsiniz .

Ya da daha programlı yapmak istiyorsanız product (replicate y x)

Söylediğim şey, sağladığı özellikleri kullanmazsanız bir stilin / dilin güçlü yönlerini göstermek zor. Ancak bu sahnelerin arkasında nasıl çalıştığını göstermek için iyi bir adım olabilir, ancak bence hangi dilde kullandığınızı en iyi şekilde kodlamanız ve daha sonra gerektiğinde neler olup bittiğini anlamak için o kişiye yardım etmelisiniz.


1
Bu cevabı diğerlerine mantıklı bir şekilde bağlamak için product, sadece işlevi olarak foldçarpma ile bir kısayol fonksiyonu ve başlangıç ​​argümanı olarak 1 ve bu replicatebir yineleyici (veya liste; üreten bir işlev) belirtilmelidir. ikisinin üstünde, haskell'de ayırt edilemez), bu da belirli sayıda aynı çıktıyı verir. Şimdi bu uygulamanın nasıl @ Jack'in yukarıdaki cevabı ile aynı şeyi yaptığını anlamak kolay olmalı, sadece daha kısa ve öz yapmak için aynı işlevlerin önceden tanımlanmış özel durum sürümlerini kullanarak.
Periata Breatta
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.