Kapanışlar saf olmayan işlevsellik olarak kabul edilir mi?


33

Kapaklar fonksiyonel programlamada saf değil midir?

Genelde değerleri doğrudan bir fonksiyona geçirerek kapaklardan kaçınılabileceği görülmektedir. Bu nedenle, mümkün olan yerlerde kapanmalardan kaçınılmalı mı?

Eğer saf değilse ve kaçınılabileceklerini belirtmekte haklıyım, neden bu kadar işlevsel programlama dili kapatmaları destekliyor?

Saf işlevin ölçütlerinden biri, "İşlev her zaman aynı argüman değerleri verilen aynı değeri değerlendirir ."

varsaymak

f: x -> x + y

f(3)her zaman aynı sonucu vermeyecektir. f(3)değeri yargümanı olmayan değerlere bağlıdır f. Böylece fsaf bir fonksiyon değildir.

Tüm kapanışlar argüman olmayan değerlere dayandığından, herhangi bir kapatmanın saf olması nasıl mümkün olabilir? Evet, teoride kapalı değer sabit olabilir, ancak sadece fonksiyonun kendisinin kaynak koduna bakarak bunu bilmenin bir yolu yoktur.

Bunun beni yönlendirdiği nokta, aynı fonksiyonun bir durumda saf, diğerinde de saf olabileceği. Bir işlevin saf olup olmadığını her zaman kaynak kodunu inceleyerek belirleyemez. Aksine, böyle bir ayrım yapılmadan önce, çağrıldığı noktadaki ortamı bağlamında düşünmek zorunda kalabilir.

Bunu doğru düşünüyor muyum?


6
Haskell'de her zaman kapak kullanıyorum ve Haskell de aldığı kadar saf.
Thomas Eding

5
Saf bir işlevsel dilde ydeğişemez, bu yüzden çıktısı f(3)her zaman aynı olacaktır.
Lily Chung,

4
yfaçıkça bir girdi olarak işaretlenmemiş olsa da tanımının bir parçasıdır f- yine de şu fşekilde tanımlanır y(f_y işlevini belirtebiliriz, açıklığa bağımlılık yapmak için y) ve bu nedenle değiştirme farklı bir işlev yverir . Belirli için tanımlanan belirli işlev , çok saftır. (Örneğin, iki işlev ve vardır farklı biz onları göstermek için aynı mektubu kullanmak gerçekleşse bile, hem saf fonksiyonlar, vb.)f_yyf: x -> x + 3f: x -> x + 5
ShreevatsaR

Yanıtlar:


26

Saflık iki şeyle ölçülebilir:

  1. Fonksiyon her zaman aynı girişi verilen aynı çıkışı verir mi? yani referans olarak şeffaf mı?
  2. İşlev kendi dışında herhangi bir şeyi değiştiriyor mu, yani yan etkileri var mı?

1 cevabı evet ve 2 cevabı hayır ise, işlev saf olacaktır. Kapaklar, yalnızca kapalı değişkeni değiştirirseniz bir işlevi kesinleştirir.


İlk madde determinizmi değil mi? Yoksa bu aynı zamanda saflığın bir parçası mı? Programlama bağlamında "saflık" kavramına pek aşina değilim.

4
@JimmyHoffa: Zorunlu değil. Bir donanım zamanlayıcısının çıktısını bir işlev içine koyabilirsiniz ve işlev dışında hiçbir şey değiştirilmez.
Robert Harvey,

1
@RobertHarvey Bütün bunlar bir fonksiyona girişleri nasıl tanımladığımızla mı ilgili? Wikipedia'dan yaptığım alıntı, işlev argümanlarına odaklanırken, kapalı değişken (ler) i de girdi olarak kabul ediyorsunuz.
user2179977

8
@ user2179977: bunlar değişken olmadıkça, sen gerektiğini değil işlevine ek girişler olarak kapalı-over değişkenleri göz önünde bulundurun. Aksine, kapatmanın kendisinin bir fonksiyon olduğunu ve farklı bir değerin üzerine kapandığında farklı bir fonksiyon olduğunu düşünmelisiniz y. Yani, örneğin bir işlev tanımlamak gböyle g(y)fonksiyon kendisi de x -> x + y. O zaman g, işlevleri g(3)döndüren bir tam sayı işlevidir, tam sayı döndüren bir tam sayı işlevidir ve tam sayı döndüren farklıg(2) bir sayı işlevidir. Her üç fonksiyon da saf.
Steve Jessop

1
@Darkhogg: Evet. Güncellememe bak.
Robert Harvey,

10

Kapanışlar, mümkün olan en saf fonksiyonel programlama şekli olan Lambda Matematik'i gösterir, bu yüzden onlara "saf olmayan" demem.

Kapanışlar "saf" değildir, çünkü işlevsel dillerdeki işlevler birinci sınıf vatandaşlardır - bu, değerler olarak ele alınabilecekleri anlamına gelir.

Bunu düşünün (sözde kodu):

foo(x) {
    let y = x + 1
    ...
}

ybir değerdir. Değeri bağlıdır x, ancak xdeğişmezdir, dolayısıyla ydeğeri de değişmezdir. fooFarklı ys üretecek farklı argümanlarla birçok kez arayabiliriz , ancak bunların yhepsi farklı kapsamlarda yaşar ve farklı slere dayanır, xböylece saflık bozulmadan kalır.

Şimdi değiştirelim:

bar(x) {
    let y(z) = x + z
    ....
}

Burada bir kapatma kullanıyoruz (x'i kapatıyoruz), ancak farklı değişkenlerle fooyapılan farklı çağrılarla aynıdır , hepsi değişmezdir, böylece saflık bozulmadan kalır.bary

Ayrıca, kapakların kurutmaya çok benzer bir etkisi olduğunu lütfen unutmayın:

adder(a)(b) {
    return a + b
}
baz(x) {
    let y = adder(x)
    ...
}

bazgerçekten farklı değil bar- ikisinde de yargümanını döndüren adlı bir fonksiyon değeri yaratırız x. Nitekim, Lambda Calculus'ta çoklu argümanlarla fonksiyonlar oluşturmak için kapaklar kullanıyorsunuz - ve bu hala saf değil.


9

Diğerleri genel soruyu cevaplarında güzel bir şekilde ele aldılar, bu yüzden sadece düzenlemenizde işaret ettiğiniz karışıklığı gidermeye bakacağım.

Kapatma, fonksiyonun bir girişi haline gelmez, aksine fonksiyon gövdesine 'gider'. Daha somut olmak gerekirse, bir işlev, vücudundaki dış kapsamdaki bir değeri ifade eder.

Fonksiyonu saflaştırdığı izlenimi altındasınız. Genel olarak durum böyle değil. İşlevsel programlamada, değerler çoğu zaman değişmezdir . Bu, kapalı değer için de geçerlidir.

Bunun gibi bir kod parçanız olduğunu varsayalım:

let make y =
    fun x -> x + y

Çağrı make 3ve make 4üzerinde kapanışları ile size iki işlevi verecek makebireyin yargüman. Biri geri dönecek x + 3, diğeri x + 4. Ancak bunlar iki farklı fonksiyondur ve her ikisi de saftır. Aynı makeişlevi kullanarak yaratıldılar , ama hepsi bu.

Not çoğu zaman bir kaç paragraf destekliyor.

  1. Saf olan Haskell'de, yalnızca değişmez değerleri kapatabilirsiniz. Kapatılacak değişken bir durum yoktur. Bu şekilde saf bir fonksiyon elde edeceğinizden eminiz.
  2. F # gibi saf işlevsel dillerde, referans hücreleri ve referans türlerini kapatabilir ve etkili bir saf olmayan işlev elde edebilirsiniz. İşlevin saf olup olmadığını bilmek için tanımlandığı kapsamı izlemeniz gerektiği konusunda haklısınız. Bir değerin bu dillerde değişip değişmediğini kolaylıkla anlayabilirsiniz, bu yüzden sorun değil.
  3. C # ve JavaScript gibi kapanışları destekleyen OOP dillerinde durum, fonksiyonel dillerin saflaştırılmasına benzer, ancak değişkenler varsayılan olarak değişkenlik gösterebildiğinden dış kapsamın izlenmesi daha zorlaşır.

2 ve 3 için, bu dillerin saflık hakkında hiçbir garanti vermediğini unutmayın. Buradaki kirlilik, kapanmanın bir özelliği değil, dilin kendisidir. Kapaklar resmi kendi başlarına değiştirmez.


1
Haskell'deki değişken değerleri kesinlikle kapatabilirsiniz, ancak böyle bir şey IO monad ile açıklanacaktır.
Daniel Gratzer

1
@ jozefg hayır, değişmez bir IO Adeğerin IO (B -> C)üzerini kapatıyorsunuz ve kapatma türünüz ya da somesuch. Saflık korunuyor
Caleth

5

Normalde sizden "saf olmayan" tanımınızı netleştirmenizi rica ediyorum, ancak bu durumda gerçekten önemli değil. Tamamen işlevsel olan terimi zıt yaptığınızı varsayarsak , cevabı "hayır" dır. Diliniz kapanmadan tamamen işlevsel olsaydı, kapanışlarla da tamamen işlevsel olurdu. Bunun yerine "işlevsel değil" anlamına geliyorsa, cevap hala "hayır" dır; kapaklar fonksiyonların oluşturulmasını kolaylaştırır.

Verileri doğrudan bir işleve ileterek genellikle kapamalardan kaçınılabileceği görülmektedir.

Evet, fakat o zaman fonksiyonunuz bir parametreye daha sahip olacak ve bu da türünü değiştirecektir. Kapaklar Eğer değişkenlere dayalı işlevler oluşturmak için izin olmadan parametreleri ekleyerek. Bu, 2 argüman alan bir işleve sahip olduğunuzda ve sadece 1 argüman alan bir sürümü oluşturmak istediğinizde kullanışlıdır.

EDIT: Kendi düzenleme / örnek ile ilgili olarak ...

varsaymak

f: x -> x + y

f (3) her zaman aynı sonucu vermez. f (3), f'nin argümanı olmayan y değerine bağlıdır. Böylece f saf bir fonksiyon değildir.

Bağımlılık burada yanlış kelime seçimidir. Yaptığınız aynı Vikipedi makalesinden alıntı:

Bilgisayar programlamasında, işlevle ilgili bu iki ifadenin de tutulması durumunda, bir işlev saf işlev olarak tanımlanabilir:

  1. İşlev her zaman aynı argüman değerleri verilen aynı sonuç değerini değerlendirir. İşlev sonucu değeri, program yürütme ilerledikçe veya programın farklı yürütmeleri arasında değişebilecek hiçbir gizli bilgiye veya duruma ya da G / Ç aygıtlarından herhangi bir harici girişe bağlı olamaz.
  2. Sonucun değerlendirilmesi, değişken nesnelerin mutasyonu veya I / O cihazlarına çıkışı gibi herhangi bir semantik olarak gözlemlenebilir yan etkiye veya çıktılığa neden olmaz.

yDeğişmez olduğu varsayılırsa (bu genellikle işlevsel dillerde geçerlidir), koşul 1 karşılanır: tüm değerleri için x, değeri f(x)değişmez. Bu, ysabit olandan farklı olmadığı ve x + 3saf olduğu gerçeğinden açıkça anlaşılmalıdır . Ayrıca, mutasyon veya G / Ç olup olmadığının da açık.


3

Çok çabuk: bir ikame, "benzer ikame benzerine yol açarsa" ise "referans olarak saydamdır" ve eğer tüm etkileri geri dönüş değerinde ise, bir işlev "saftır". Her ikisi de kesin olarak yapılabilir, ancak aynı olmadıklarını ve hatta birinin diğerini ima etmediğini not etmek çok önemlidir.

Şimdi kapaklar hakkında konuşalım.

Sondaj (çoğunlukla saf) "kapaklar"

Kapamalar bir lambda terimini değerlendirirken değişkenleri çevre aramaları olarak yorumladığımız için oluşur. Dolayısıyla, bir değerlendirme sonucu bir lambda terimi döndürdüğümüzde, içindeki değişkenler tanımlandıklarında aldıkları değerleri "kapatır".

Düz lambda matematiğinde bu önemsiz bir şeydir ve bütün kavram ortadan kalkar. Bunu göstermek için, işte göreceli olarak hafif bir lambda hesabı tercümanı:

-- untyped lambda calculus values are functions
data Value = FunVal (Value -> Value)

-- we write expressions where variables take string-based names, but we'll
-- also just assume that nobody ever shadows names to avoid having to do
-- capture-avoiding substitutions

type Name = String

data Expr
  = Var Name
  | App Expr Expr
  | Abs Name Expr

-- We model the environment as function from strings to values, 
-- notably ignoring any kind of smooth lookup failures
type Env = Name -> Value

-- The empty environment
env0 :: Env
env0 _ = error "Nope!"

-- Augmenting the environment with a value, "closing over" it!
addEnv :: Name -> Value -> Env -> Env
addEnv nm v e nm' | nm' == nm = v
                  | otherwise = e nm

-- And finally the interpreter itself
interp :: Env -> Expr -> Value
interp e (Var name) = e name          -- variable lookup in the env
interp e (App ef ex) =
  let FunVal f = interp e ef
      x        = interp e ex
  in f x                              -- application to lambda terms
interp e (Abs name expr) =
  -- augmentation of a local (lexical) environment
  FunVal (\value -> interp (addEnv name value e) expr)

addEnvDikkat edilmesi gereken önemli nokta , çevreyi yeni bir adla genişlettiğimizde. Bu fonksiyon, yorumlanmış Absçekiş teriminin yalnızca "iç" olarak adlandırılır (lambda terimi). Çevre biz değerlendirmek ne zaman "baktım" alır Varvadeli ve bu nedenle Vars çözmek ne kadar Nameatıfta Envhangi tarafından yakalanan var Absiçeren çekiş Var.

Şimdi, yine, düz LC terimlerinde bu çok sıkıcı. Bağımlı değişkenlerin, herkesin umursadığı ölçüde sabit olduğu anlamına gelir. Doğrudan ve hemen, çevrede belirttikleri değerler, o noktaya kadar sözcüksel olarak kapsamlaştırıldığı gibi değerlendirilir.

Bu da (neredeyse) saf. Bizim lambda hesabımızdaki herhangi bir terimin tek anlamı, geri dönüş değeri ile belirlenir. Tek istisna, Omega terimi tarafından somutlaştırılan fesih olmamanın yan etkisidir:

-- in simple LC syntax:
--
-- (\x -> (x x)) (\x -> (x x))
omega :: Expr
omega = App (Abs "x" (App (Var "x") 
                          (Var "x")))
            (Abs "x" (App (Var "x") 
                          (Var "x")))

İlginç (saf olmayan) kapanışlar

Şimdi, belirli geçmişlere göre, yukarıda düz LC'de açıklanan kapaklar sıkıcıdır, çünkü kapattığımız değişkenlerle etkileşime girme fikri yoktur. Özellikle, "kapatma" kelimesi aşağıdaki Javascript gibi bir kod çağırır.

> function mk_counter() {
  var n = 0;
  return function incr() {
    return n += 1;
  }
}
undefined

> var c = mk_counter()
undefined
> c()
1
> c()
2
> c()
3

Bu n, iç işlevdeki değişkeni kapattığımızı incrve çağrının incrbu değişkenle anlamlı bir şekilde etkileşime girdiğimizi gösterir . mk_countersaf, ancak incrkesin olarak kesin değildir (ve referans olarak şeffaf değildir).

Bu iki örnek arasında ne fark var?

"Değişken" kavramları

Düz LC anlamında yer değiştirme ve soyutlamanın ne anlama geldiğine bakarsak, kesinlikle düz olduklarını fark ediyoruz. Değişkenler tam anlamıyla acil ortam aramalarından başka bir şey değildir . Lambda soyutlama kelimenin tam anlamıyla iç ifadeyi değerlendirmek için artırılmış bir ortam yaratmaktan başka bir şey değildir . Bu modelde gördüğümüz davranış türüne yer yok mk_counter/ var incrçünkü izin verilebilecek bir değişiklik yok.

Birçokları için bu, "değişken" in ne anlama geldiğinin kalbidir - çeşitlilik. Bununla birlikte, anlambilimciler, LC'de kullanılan değişken türüyle Javascript'te kullanılan "değişken" türünü birbirinden ayırmayı severler. Bunu yapmak için, ikincisini "değiştirilebilir hücre" veya "yarık" olarak adlandırmaya meyillidirler.

Bu terminoloji, "değişken" in "bilinmeyen" gibi bir şey ifade ettiği matematikteki uzun tarihsel kullanımını izler: "x + x" (matematiksel) ifadesi xzaman içinde değişime izin vermez , bunun yerine anlamsız olması gerekir. (tek, sabit) değerinin xalır.

Bu nedenle, değerleri bir yuvaya koyma ve bunları dışarıya çıkarma yeteneğini vurgulamak için “yuva” diyoruz.

Kargaşayı daha da eklemek için Javascript'te bu "slotlar" değişkenlerle aynı gözüküyor:

var x;

birini oluşturmak ve sonra yazarken

x;

o anda o yuvada depolanmış olan değeri aradığımızı gösterir. Bunu daha net hale getirmek için, saf diller slotları (matematiksel, lambda matematiği) isimleri olarak alan isimler olarak kabul etme eğilimindedir. Bu durumda, bir slottan çıktığımızda veya çıkardığımızda açıkça etiketlemeliyiz. Böyle bir gösterim gibi görünmek eğilimindedir

-- create a fresh, empty slot and name it `x` in the context of the 
-- expression E
let x = newSlot in E

-- look up the value stored in the named slot named `x`, return that value
get x

-- store a new value, `v`, in the slot named `x`, return the slot
put x v

Bu gösterimin avantajı, şimdi matematiksel değişkenler ve değişken yuvalar arasında kesin bir ayrım yapmamızdır. Değişkenler, değerleri değer olarak alabilir, ancak değişken tarafından adlandırılan belirli alan, kapsamı boyunca sabittir.

Bu gösterimi kullanarak mk_counterörneği yeniden yazabiliriz (bu sefer Haskell benzeri bir anlambilimine karar vermemiş olsa da Haskell benzeri bir sözdiziminde):

mkCounter = 
  let x = newSlot 
  in (\() -> let old = get x 
             in get (put x (old + 1)))

Bu durumda, bu değişken yuvayı manipüle eden prosedürleri kullanıyoruz. Uygulayabilmek için, yalnızca sabit bir ad ortamını xdeğil aynı zamanda gerekli tüm yuvaları içeren değişken bir ortamı kapatmamız gerekir . Bu, ortak "kapatma" kavramına daha yakın olan insanlar çok seviyor.

Yine, mkCounterçok saf değil. Aynı zamanda referans olarak opaktır. Ama bildirim yan etkileri adı yakalama veya kapatılması yerine yakalanması kaynaklanan olmadığını değişken hücreye ve benzeri üzerinde yan etkileyen operasyonlar getve put.

Sonuçta, bunun sorunuzun son cevabı olduğunu düşünüyorum: saflık (matematiksel) değişken yakalamadan etkilenmez, bunun yerine yakalanan değişkenler tarafından adlandırılan değişken yuvalarda gerçekleştirilen yan etkileyici işlemlerden etkilenir.

Sadece, LC'ye yakın olmaya çalışmayan veya bu iki kavramın sık sık karıştığı karışıklığı sağlayacak saflığı korumaya çalışmayan dillerde.


1

Hayır, kapaklar, kapalı değer sabit olduğu sürece (kapatma ya da başka bir kodla değiştirilmez), işlevsel programlamadaki olağan durum olan bir fonksiyonun bozulmasına neden olmaz.

Bir değeri her zaman argüman olarak iletebilirken, genellikle bunu büyük miktarda zorluk çekmeden yapamayacağınızı unutmayın. Örneğin (coffeescript):

closedValue = 42
return (arg) -> console.log "#{closedValue} #{arg}"

Önerinize göre, sadece geri dönebilirsiniz:

return (arg, closedValue) -> console.log "#{closedValue} #{arg}"

Bu işlev bu noktada çağrılmaz , yeni tanımlanır , bu nedenle işlevin çağrıldığı noktaya istediğiniz değeri iletmek için bir yol bulmanız gerekir closedValue. En iyi ihtimalle bu çok fazla kuplaj yaratır. En kötüsü, kodu arama noktasında kontrol edemezsiniz, bu yüzden etkili bir şekilde imkansızdır.

Kapanmayı desteklemeyen dillerdeki etkinlik kütüphaneleri genellikle keyfi verileri geri aramaya geri göndermenin başka bir yolunu sunar, ancak bu hoş değildir ve hem kütüphane sağlayıcısı hem de kütüphane kullanıcıları için çok fazla karmaşıklık yaratır.

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.