Değişmez yapılar ve derin kompozisyon hiyerarşisi


9

Grafiklerle yoğun bir şekilde çalışan bir GUI uygulaması geliştiriyorum - örnek için bir vektör editörü olarak düşünebilirsiniz. Tüm veri yapılarını değişmez kılmak çok caziptir - bu yüzden neredeyse çaba harcamadan geri alabilir / yineleyebilir, kopyalayabilir / yapıştırabilir ve daha birçok şey yapabilirim.

Basitlik uğruna, aşağıdaki örneği kullanacağım - uygulama çokgen şekilleri düzenlemek için kullanılır, bu yüzden sadece değişmez noktaların listesi olan "Çokgen" nesnesine sahibim:

Scene -> Polygon -> Point

Ve böylece programımda sadece bir değişken değişkenim var - geçerli Scene nesnesini tutan değişken. Nokta sürüklemeyi uygulamaya çalıştığımda başladığım sorun - değişebilir sürümde, sadece bir Pointnesneyi alıyorum ve koordinatlarını değiştirmeye başlıyorum. Değişmez versiyonda - sıkıştım. Ben endeksleri depolanan olabilirdi Polygonakım Sceneiçinde sürüklenen noktanın, endeks Polygonve her zaman değiştirin. Ancak bu yaklaşım ölçeklenmez - kompozisyon seviyeleri 5'e ve daha fazlasına gittiğinde, kazan plakası dayanılmaz hale gelir.

Eminim bu problem çözülebilir - sonuçta tamamen değişmez yapılara ve IO monad'a sahip Haskell var. Ama nasıl olduğunu bulamıyorum.

Bana bir ipucu verebilir misin?


@ İş - şu anda böyle çalışıyor ve bana çok acı veriyor. Bu yüzden alternatif yaklaşımlar arıyorum - ve en azından buna kullanıcı etkileşimi eklemeden önce, bu uygulama yapısı için değişmezlik mükemmel görünüyor :)
Rogach

@ Roach: Isıtıcı kodunuz hakkında daha fazla bilgi verebilir misiniz?
rwong

Yanıtlar:


9

Geçerli Sahnede Çokgen indekslerini, Çokgen'de sürüklenen noktanın dizinini saklayabilirim ve her seferinde değiştirebilirim. Ancak bu yaklaşım ölçeklenmez - kompozisyon seviyeleri 5'e ve daha fazlasına gittiğinde, kazan plakası dayanılmaz hale gelir.

Kesinlikle haklısın, kazan plakasının etrafından geçemezsen bu yaklaşım ölçeklendirilmez . Özellikle, küçük bir alt bölüme sahip yepyeni bir Sahne oluşturmak için ısıtıcı plaka değişti. Bununla birlikte, birçok işlevsel dil bu tür iç içe yapı manipülasyonuyla uğraşmak için bir yapı sağlar: lensler.

Lens temelde değişmez veriler için bir alıcı ve ayarlayıcıdır. Bir lens, daha büyük bir yapının bazı küçük kısımlarına odaklanır . Bir lens verildiğinde, onunla yapabileceğiniz iki şey vardır - daha büyük bir yapının değerinin küçük kısmını görüntüleyebilir veya daha büyük bir yapının değerinin küçük kısmını yeni bir değere ayarlayabilirsiniz . Örneğin, listedeki üçüncü öğeye odaklanan bir lensiniz olduğunu varsayalım:

thirdItemLens :: Lens [a] a

Bu tür, daha büyük yapının bir şeyler listesi olduğu ve küçük alt bölüm de bu şeylerden biri olduğu anlamına gelir. Bu lens verildiğinde listedeki üçüncü öğeyi görüntüleyebilir ve ayarlayabilirsiniz:

> view thirdItemLens [1, 2, 3, 4, 5]
3
> set thirdItemLens 100 [1, 2, 3, 4, 5]
[1, 2, 100, 4, 5]

Lenslerin yararlı olmasının nedeni, alıcıları ve ayarlayıcıları temsil eden değerlerdir ve diğer değerlerle aynı şekilde onları soyutlayabilirsiniz. Mercekleri döndüren işlevler yapabilirsiniz, örneğin listItemLensbir numarayı alıp listedeki öğeyi ngörüntüleyen merceği döndüren bir işlev n. Ayrıca, lensler edilebilir oluşur :

> firstLens = listItemLens 0
> thirdLens = listItemLens 2
> firstOfThirdLens = lensCompose firstLens thirdLens
> view firstOfThirdLens [[1, 2], [3, 4], [5, 6], [7, 8]]
5
> set firstOfThirdLens 100 [[1, 2], [3, 4], [5, 6], [7, 8]]
[[1, 2], [3, 4], [100, 6], [7, 8]]

Her mercek, veri yapısının bir seviyesinin üzerinden geçme davranışını kapsar. Bunları birleştirerek, karmaşık yapıların birden fazla seviyesini geçmek için kazan plakasını ortadan kaldırabilirsiniz. Örneğin, supposing sahip bir scenePolygonLens io görüşleri ibir bir Scene Poligon ve inci polygonPointLens ngördüğünü nthbir Poligon içinde Noktası, size şöyle bütün bir sahnede yalnızca önemsediğiniz belirli bir noktayı odaklanarak için objektif yapıcısı yapabilirsiniz:

scenePointLens i n = lensCompose (polygonPointLens n) (scenePolygonLens i)

Şimdi bir kullanıcının çokgen 14'ün 3. noktasını tıkladığını ve 10 piksel sağa taşıdığını varsayın. Sahnenizi şu şekilde güncelleyebilirsiniz:

lens = scenePointLens 14 3
point = view lens currentScene
newPoint = movePoint 10 0 point
newScene = set lens newPoint currentScene

Bu lens, içindeki bir Sahneyi gezmek ve güncellemek için tüm kazan levhalarını içerir , tek yapmanız gereken noktayı değiştirmek istediğiniz şeydir. Bunu lensTransformbir lensi, bir hedefi kabul eden bir işlevle ve hedefin lens aracılığıyla görünümünü güncellemek için bir işlevle daha da soyutlayabilirsiniz :

lensTransform lens transformFunc target =
  current = view lens target
  new = transformFunc current
  set lens new target

Bu, bir işlevi alır ve işlevi yalnızca görünüme uygulayarak ve yeni bir görünüm oluşturmak için kullanarak karmaşık bir veri yapısında "güncelleyici" haline getirir. Böylece, 14. çokgenin 3. noktasını sağ 10 piksele taşıma senaryosuna geri dönelim, bu şu şekilde ifade edilebilir lensTransform:

lens = scenePointLens 14 3
moveRightTen point = movePoint 10 0 point
newScene = lensTransform lens moveRightTen currentScene

Ve tüm sahneyi güncellemek için ihtiyacınız olan bu. Bu çok güçlü bir fikirdir ve değer verdiğiniz veri parçalarını görüntüleyen lensler oluşturmak için bazı güzel işlevleriniz olduğunda çok iyi çalışır.

Bununla birlikte, tüm bunlar işlevsel programlama topluluğunda bile oldukça dışarıda. Lenslerle çalışmak için iyi bir kütüphane desteği bulmak zor ve nasıl çalıştıklarını ve iş arkadaşlarınız için faydalarını açıklamak daha da zor. Bu yaklaşımı bir tuz tanesi ile alın.


Mükemmel açıklama! Şimdi lenslerin ne olduğunu anladım!
Vincent Lecrubier

13

Aynı problem üzerinde çalıştım (ancak sadece 3 kompozisyon seviyesiyle). Temel fikir klonlamak, sonra değiştirmek . Değişmez programlama tarzında, klonlama ve modifikasyonun birlikte gerçekleşmesi gerekir, bu da komut nesnesi haline gelir .

Değişken programlama tarzında, klonlamanın yine de gerekli olacağını unutmayın:

  • Geri alma / yineleme işlemine izin vermek için
  • Görüntüleme sisteminin, kullanıcının değişiklikleri görebilmesi için üst üste bindirilen (hayalet çizgiler olarak) "düzenleme öncesi" ve "düzenleme sırasında" modelini aynı anda görüntülemesi gerekebilir.

Değişken programlama tarzında,

  • Mevcut yapı derin klonlanmıştır
  • Değişiklikler klonlanmış kopyada yapılır
  • Ekran motorunun eski yapıyı hayalet çizgilerle ve klonlanmış / değiştirilmiş yapıyı renkli hale getirmesi söylenir.

Değişmez programlama tarzında,

  • Veri değişikliği ile sonuçlanan her kullanıcı eylemi, bir "komut" dizisiyle eşlenir.
  • Bir komut nesnesi tam olarak hangi modifikasyonun uygulanacağını ve orijinal yapıya bir referans içerir.
    • Benim durumumda, komut nesnem yalnızca değiştirilmesi gereken nokta dizinini ve yeni koordinatları hatırlıyor. (yani çok hafif, çünkü kesinlikle değişmez tarzı takip etmiyorum.)
  • Bir komut nesnesi yürütüldüğünde, yapının değiştirilmiş bir derin kopyasını oluşturur, bu da değişikliği yeni kopyada kalıcı hale getirir.
  • Kullanıcı daha fazla düzenleme yaparken, daha fazla komut nesnesi oluşturulur.

1
Değişmez bir veri yapısının neden derin bir kopyasını çıkarmalıyım? Sadece referansların "omurgasını" değiştirilmiş nesneden köke kopyalamanız ve orijinal yapının geri kalan kısımlarına referansları korumanız yeterlidir.
Monica

3

Derin değişmez nesneler, bir şeyi derin klonlamanın basitçe bir referans kopyalamayı gerektirmesi avantajına sahiptir. Dezavantajı, derin yuvalanmış bir nesnede küçük bir değişiklik yapmanın, içinde yuvalandığı her nesnenin yeni bir örneğini oluşturmayı gerektirmesidir. Değişken nesneler, bir nesneyi değiştirmenin kolay olması - sadece yap - ancak bir nesnenin derin klonlanması, her iç içe nesnenin derin bir klonunu içeren yeni bir nesne inşa etmeyi gerektirir. Bir, bir nesneyi klonlamak ve bir değişiklik yapmak, klon nesne, başka değişiklik yapmak istiyorsa, kötüsü vb sonra ne kadar büyük ya da küçük değişiklikler biri zorunda olan tutmak her kaydedilmiş versiyonu için bütün hiyerarşisi bir kopyasını nesnenin durumu. Pis.

Dikkate değer bir yaklaşım, değişebilir ve derin değişmez türev türleriyle soyut bir "belki Değişebilir" tür tanımlamak olacaktır. Tüm bu tipler bir AsImmutableyönteme sahiptir; bu yöntemin bir nesnenin derinden değişmeyen bir örneği üzerinde çağrılması, o örneği döndürecektir. Bunu değişebilir bir örnekte çağırmak, özellikleri orijinalindeki eşdeğerlerinin derinden değişmez anlık görüntüleri olan derinden değişmez bir örnek döndürür. Değişken eşdeğeri olan değişmez tipler AsMutable, özellikleri orijinal ile aynı olan değişebilir bir örnek oluşturacak bir yöntem kullanır.

İçten değiştirilemeyen bir nesnede iç içe geçmiş bir nesnenin değiştirilmesi, ilk olarak dış değiştirilemez nesnenin değiştirilebilir bir nesneyle değiştirilmesini, daha sonra değiştirilecek şeyi içeren özelliğin değişebilir bir şeyle vb. Değiştirilmesini, ancak genel nesnenin, AsImmutabledeğişebilir bir nesneyi çağırma girişiminde bulunuluncaya kadar (değiştirilebilen nesneleri değişebilir bırakacak, ancak aynı verileri tutan değişmez nesneleri döndürecek) herhangi bir ek nesne yapılmasını gerektirmeyecektir .

Basit ancak önemli optimizasyonlar olarak, her değişebilen nesne, ilişkili değişmez tipindeki bir nesneye önbelleğe alınmış bir referans tutabilir ve her değişmez tip, GetHashCodedeğerini önbelleğe almalıdır . AsImmutableDeğişken bir nesneyi çağırırken , yeni bir değişmez nesneyi döndürmeden önce, önbelleğe alınan referansla eşleşip eşleşmediğini kontrol edin. Öyleyse, önbelleğe alınan referansı döndürün (yeni değiştirilemeyen nesneyi terk etme). Aksi takdirde, önbelleğe alınan başvuruyu yeni nesneyi tutacak şekilde güncelleyin ve geri gönderin. Bu yapılırsa, tekrarlanan çağrılarAsImmutablearaya giren mutasyonlar olmadan aynı nesne referanslarını verecektir. Yeni örnekleri oluşturma maliyetinden tasarruf edilmese bile, bunları tutmanın bellek maliyetinden kaçınılacaktır. Ayrıca, değiştirilen nesneler arasında eşitlik karşılaştırmaları, çoğu durumda karşılaştırılan öğeler referans-eşitse veya farklı karma kodlarına sahipse, büyük ölçüde hızlandırılabilir.

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.