Sayısal algoritmaları kapsüllemek için sınıfları kullanmanın avantajları ve dezavantajları nelerdir?


13

Bilimsel hesaplamada kullanılan algoritmaların çoğu, daha az matematik ağırlıklı yazılım mühendisliği formlarında ele alınan algoritmalardan farklı bir doğal yapıya sahiptir. Özellikle, bireysel matematiksel algoritmalar oldukça karmaşık olma eğilimindedir, genellikle yüzlerce veya binlerce satırlık satır içerir, ancak yine de hiçbir durum içermez (yani karmaşık bir veri yapısına etki etmez) ve genellikle programlı olarak kaynatılabilir arayüz - bir dizi (veya iki) üzerinde çalışan tek bir işleve.

Bu, bir sınıfın değil, bir fonksiyonun bilimsel hesaplamada karşılaşılan çoğu algoritmanın doğal arayüzü olduğunu göstermektedir. Ancak bu argüman, karmaşık, çok parçalı algoritmaların uygulanmasının nasıl ele alınması gerektiğine dair çok az fikir vermektedir.

Geleneksel yaklaşım, bir dizi başka işlevi çağıran ve ilgili argümanları yol boyunca geçiren bir işleve sahip olmak olsa da, OOP farklı bir yaklaşım sunar, burada algoritmalar sınıf olarak kapsüllenebilir. Açıklık için, bir algoritmayı bir sınıfa kapsüllemek suretiyle, algoritma girişlerinin sınıf yapıcısına girildiği ve daha sonra algoritmayı gerçekten çağırmak için genel bir yöntem çağrıldığı bir sınıf yaratmak anlamına gelir. C ++ psuedocode içinde multigrid'in böyle bir uygulaması aşağıdaki gibi görünebilir:

class multigrid {
    private:
        x_, b_
        [grid structure]

        restrict(...)
        interpolate(...)
        relax(...)
    public:
        multigrid(x,b) : x_(x), b_(b) { }
        run()
}

multigrid::run() {
     [call restrict, interpolate, relax, etc.]
}

O zaman sorum şu: sınıfsız daha geleneksel bir yaklaşımla karşılaştırıldığında bu tür uygulamanın faydaları ve dezavantajları nelerdir? Genişletilebilirlik veya sürdürülebilirlik sorunları var mı? Açık olmak gerekirse, görüş almak istemiyorum, daha ziyade böyle bir kodlama uygulamasını benimsemenin aşağı yönlü etkilerini (yani bir kod tabanı oldukça büyük hale gelene kadar ortaya çıkamayan) daha iyi anlamak istiyorum.


2
Sınıf adınız bir ad yerine bir sıfat olduğunda her zaman kötü bir işarettir.
David Ketcheson

3
Bir sınıf, karmaşıklığı yönetmek için işlevleri düzenlemek için durum bilgisi olmayan bir ad alanı görevi görebilir, ancak sınıfları sağlayan dillerde karmaşıklığı yönetmenin başka yolları da vardır. (C ++ 'daki ad alanları ve Python'daki modüller akla geliyor.)
Geoff Oxberry

@GeoffOxberry Bunun iyi mi yoksa kötü kullanım mı olduğunu söyleyemem - bu yüzden ilk etapta soruyorum - ancak sınıflar, ad alanlarının veya modüllerin aksine "geçici durumu", ör. Izgara hiyerarşisini yönetebilir multigrid'de, algoritmanın tamamlanmasından sonra atılır.
Ben

Yanıtlar:


13

Sayısal yazılımları 15 yıl boyunca yaptıktan sonra, aşağıdakileri açıkça ifade edebilirim:

  • Kapsülleme önemlidir. Veri depolama düzenini açığa çıkardığından, işaretçilerin etrafından verilere (önerdiğiniz gibi) geçmek istemezsiniz. Depolama düzenini açığa çıkarırsanız, bir daha asla değiştiremezsiniz çünkü tüm programdaki verilere erişeceksiniz. Bundan kaçınmanın tek yolu, verileri bir sınıfın özel üye değişkenlerine kapsüllemek ve yalnızca üye işlevlerin üzerinde hareket etmesine izin vermektir. Sorunuzu okursam, bir matrisin özdeğerlerini vatansız olarak hesaplayan, matris girişlerine argüman olarak bir işaretçi alarak ve özdeğerleri bir şekilde döndüren bir işlev düşünürsünüz. Bunun hakkında düşünmenin yanlış yolu olduğunu düşünüyorum. Benim görüşüme göre, bu işlev bir sınıfın "const" üyesi işlevi olmalıdır - matrisi değiştirdiği için değil, verilerle çalışan bir işlev olduğu için.

  • Çoğu OO programlama dili özel üye işlevlerine sahip olmanızı sağlar. Büyük bir algoritmayı daha küçük bir algoritmaya ayırmanın yolu budur. Örneğin, özdeğer hesaplaması için ihtiyacınız olan çeşitli yardımcı işlevler hala matris üzerinde çalışır ve bu nedenle doğal olarak bir matris sınıfının özel üye işlevleri olacaktır.

  • Diğer birçok yazılım sistemiyle karşılaştırıldığında, sınıf hiyerarşilerinin, örneğin grafiksel kullanıcı arayüzlerinde genellikle daha az önemli olduğu doğru olabilir. Sayısal yazılımda belirgin oldukları yerler kesinlikle vardır - Jed, bu iş parçacığının başka bir cevabında, yani bir matrisi temsil etmenin birçok yolu (veya daha genel olarak, sonlu boyutlu vektör uzayında doğrusal bir operatör) ana hatlarıyla belirtmektedir. PETSc bunu matrislere etki eden tüm işlemler için sanal işlevlerle çok tutarlı bir şekilde yapar (buna "sanal işlevler" demezler, ancak budur). Tipik sonlu eleman kodlarında OO yazılımının bu tasarım prensibini kullanan başka alanlar da vardır. Akla gelenler birçok çeşit kareleme formülü ve birçok çeşit sonlu elementtir, bunların hepsi doğal olarak tek bir arayüz / birçok uygulama olarak temsil edilmektedir. Maddi hukuk tanımları da bu gruba girer. Ancak bununla ilgili olduğu ve sonlu eleman kodunun geri kalanının kalıtımı, örneğin GUI'lerde kullanabileceği kadar yaygın olarak kullanmadığı doğru olabilir.

Sadece bu üç noktadan itibaren, nesne yönelimli programlamanın sayısal kodlar için de kesinlikle geçerli olduğu ve bu stilin birçok faydasını görmezden gelmenin aptalca olacağı açık olmalıdır. BLAS / LAPACK'in bu paradigmayı kullanmadığı (ve MATLAB'ın maruz kaldığı normal arayüzün de kullanmadığı) doğru olabilir, ancak son 10 yılda yazılan her başarılı sayısal yazılımın aslında, nesne odaklı.


16

Kapsülleme ve veri gizleme, bilimsel hesaplamadaki genişletilebilir kütüphaneler için son derece önemlidir . Matrisleri ve doğrusal çözücüleri iki örnek olarak ele alalım. Bir kullanıcının sadece bir operatörün doğrusal olduğunu bilmesi gerekir, ancak sparisite, bir çekirdek, hiyerarşik bir temsil, bir tensör ürünü veya bir Schur tamamlayıcısı gibi iç yapıya sahip olabilir. Her durumda, Krylov yöntemleri operatörün ayrıntılarına bağlı değildir, sadece MatMultişlevin eylemine (ve belki de bitişikine) bağlıdır. Benzer şekilde, doğrusal çözücü arabiriminin kullanıcısı (örneğin doğrusal olmayan bir çözücü) yalnızca doğrusal sorunun çözüldüğünü umursar ve kullanılan algoritmayı belirtmesi veya belirtmesi gerekmez. Gerçekten, bu tür şeyleri belirtmek, doğrusal olmayan çözücünün (veya başka bir dış arabirimin) kapasitesini engelleyecektir.

Arayüzler iyidir. Bir uygulamaya bağlı olarak kötü. Bunu C ++ sınıflarını, C nesnelerini, Haskell tiplerini veya başka bir dil özelliğini kullanarak başarmanız önemsizdir. Bir arayüzün yeteneği, sağlamlığı ve genişletilebilirliği bilimsel kütüphanelerde önemli olan şeydir.


8

Sınıflar yalnızca kodun yapısı hiyerarşikse kullanılmalıdır. Algoritmalardan bahsettiğiniz için, doğal yapıları bir nesne hiyerarşisi değil, bir akış şemasıdır.

OpenFOAM durumunda, algoritmik bölüm, farklı türdeki sayısal şemalar kullanılarak, farklı tipte tansörler üzerinde çalışan soyut fonksiyonlar olan jenerik operatörler (div, grad, curl, vb.) Olarak uygulanır. Kodun bu kısmı temelde sınıflar üzerinde çalışan bir çok genel algoritmadan yapılmıştır. Bu, istemcinin şöyle bir şey yazmasına izin verir:

solve(ddt(U) + div(phi, U)  == rho*g + ...);

Taşıma modelleri, türbülans modelleri, fark şemaları, gradyan şemaları, sınır koşulları, vb. Gibi hiyerarşiler C ++ sınıfları (yine tensör miktarları için genel) açısından uygulanır.

CGAL kütüphanesinde benzer bir yapı fark ettim, burada çeşitli algoritmalar geometrik Çekirdekler (sınıflar) oluşturmak için geometrik bilgilerle paketlenmiş fonksiyon nesneleri grubu olarak bir araya getirilir, ancak bu yine işlemleri geometriden ayırmak için yapılır (nokta kaldırma nokta veri türünden bir yüz).

Hiyerarşik yapı ==> sınıflar

Yordamsal, akış şeması ==> algoritmalar


5

Bu eski bir soru olsa bile, Julia'nın özel çözümünden bahsetmeye değer olduğunu düşünüyorum . Bu dilin yaptığı "sınıfsız OOP" dir: ana yapılar türlerdir, yani structmiras ilişkisinin tanımlandığı C cinsinden benzer bileşik veri nesneleridir . Türlerin "üye işlevleri" yoktur, ancak her işlevin bir tür imzası vardır ve alt türleri kabul eder. Örneğin, soyut olabilir Matrixtipi ve alt türlerini DenseMatrix, SparseMatrixve genel bir yöntem var do_something(a::Matrix, b::Matrix)uzmanlaşma ile do_something(a::SparseMatrix, b::SparseMatrix). Aramak için en uygun sürümü seçmek için çoklu dağıtım kullanılır.

Bu yaklaşım, "bir yöntemin thisilk parametresi olarak bir işlev olduğu" (örneğin Python'da ortak) kuralını kabul ederseniz, yalnızca ilk argümandaki mirasa dayalı gönderime eşdeğer olan sınıf tabanlı OOP'dan daha güçlüdür . Bir çeşit çoklu gönderim, örneğin C ++ 'da benzetilebilir, ancak önemli ölçüde bükülmelerle .

Temel ayrım, yöntemlerin sınıflara ait olmaması, ancak ayrı parametreler olarak var olmaları ve tüm parametrelerde miras olabileceği yönündedir.

Bazı referanslar:

http://docs.julialang.org/en/release-0.4/manual/methods/

http://assoc.tumblr.com/post/71454527084/cool-things-you-can-do-in-julia

https://thenewphalls.wordpress.com/2014/03/06/understanding-object-oriented-programming-in-julia-inheritance-part-2/


1

OO yaklaşımının iki avantajı şunlar olabilir:

  • farklı sonuçlar isteyebileceğiniz veya istemeyebileceğiniz uzun bir hesaplama. Örneğin, son çıktıysa, ancak ara sonuca bağlıysa , örnek içinde sonucunu önbelleğe alan bir yönteminiz olabilir . Arama yaptığınızda , henüz sonucu önbelleğe alınmadıysa da çağırır .α α αβαcalculate_alpha()αcalculate_beta()calculate_alpha()α

  • birden fazla girişe sahip bir hesaplama, burada bir giriş değişirse, tüm hesaplamanın mutlaka yapılması gerekmez. Örneğin, calculate_f()yöntem döndürür . Daha sonra, başka bir değeri için hesaplamayı yeniden yapmaya karar verirseniz , arayabilir ve parametresi dahili olarak 'kirli' olarak işaretlenir, böylece tekrar aradığınızda , hesaplamanın yalnızca bağlı kısmı yeniden yapılır.z z zf(x,y,z)zset_z()zcalculate_f()z

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.