Genel Bakış
Tür düzeyinde programlamanın geleneksel, değer düzeyinde programlamayla birçok benzerliği vardır. Ancak, hesaplamanın çalışma zamanında gerçekleştiği değer düzeyinde programlamanın aksine, tür düzeyinde programlamada hesaplama derleme zamanında gerçekleşir. Değer düzeyinde programlama ile tür düzeyinde programlama arasında paralellikler kurmaya çalışacağım.
Paradigmalar
Tür düzeyinde programlamada iki ana paradigma vardır: "nesne yönelimli" ve "işlevsel". Buradan bağlantılı örneklerin çoğu, nesne yönelimli paradigmayı takip eder.
Nesne yönelimli paradigmadaki tür düzeyinde programlamanın iyi, oldukça basit bir örneği, apocalisp'in lambda hesabının uygulamasında bulunabilir , burada çoğaltılır:
// Abstract trait
trait Lambda {
type subst[U <: Lambda] <: Lambda
type apply[U <: Lambda] <: Lambda
type eval <: Lambda
}
// Implementations
trait App[S <: Lambda, T <: Lambda] extends Lambda {
type subst[U <: Lambda] = App[S#subst[U], T#subst[U]]
type apply[U] = Nothing
type eval = S#eval#apply[T]
}
trait Lam[T <: Lambda] extends Lambda {
type subst[U <: Lambda] = Lam[T]
type apply[U <: Lambda] = T#subst[U]#eval
type eval = Lam[T]
}
trait X extends Lambda {
type subst[U <: Lambda] = U
type apply[U] = Lambda
type eval = X
}
Örnekte görülebileceği gibi, tür düzeyinde programlama için nesne yönelimli paradigma şu şekilde ilerler:
- İlk olarak: çeşitli soyut tip alanlarla soyut bir özellik tanımlayın (soyut alanın ne olduğunu görmek için aşağıya bakın). Bu, bir uygulamayı zorlamadan tüm uygulamalarda belirli tür alanların var olduğunu garanti eden bir şablondur. Lambda taşı örnekte, bu karşılık
trait Lambda
bu garantiler aşağıdaki türleri mevcut olduğunu: subst
, apply
ve eval
.
- Ardından: soyut özelliği genişleten ve çeşitli soyut tür alanlarını uygulayan alt çizgiler tanımlayın
- Genellikle, bu alt çizgiler bağımsız değişkenlerle parametreleştirilir. Lambda hesabı örneğinde, iki türle
trait App extends Lambda
parametreleştirilen ( S
ve T
her ikisi de alt türleri olmalıdır Lambda
), bir türle ( ) trait Lam extends Lambda
parametreleştirilmiş T
ve trait X extends Lambda
(parametreleştirilmemiş) alt türlerdir .
- tür alanları genellikle alt yüzeyin tür parametrelerine atıfta bulunularak ve bazen hash operatörü
#
(nokta operatörüne çok benzer: .
değerler için) aracılığıyla tür alanlarına başvurarak uygulanır . Özellik olarak App
lambda taşı örneğin, türü eval
, aşağıdaki gibi uygulanır: type eval = S#eval#apply[T]
. Bu aslında eval
özelliğin parametresinin türünü ve sonuçta parametre ile S
çağırmaktır . Not, bir türe sahip olma garantilidir çünkü parametre bunun bir alt türü olduğunu belirtir . Benzer şekilde, sonucu , soyut özellikte belirtildiği gibi, bir alt türü olarak belirtildiğinden, bir türe sahip olmalıdır .apply
T
S
eval
Lambda
eval
apply
Lambda
Lambda
İşlevsel paradigma, özelliklerde birlikte gruplandırılmamış çok sayıda parametreleştirilmiş tür kurucusunun tanımlanmasından oluşur.
Değer düzeyinde programlama ve tür düzeyinde programlama arasında karşılaştırma
- soyut sınıf
- değer düzeyi:
abstract class C { val x }
- tür düzeyi:
trait C { type X }
- yola bağlı türler
C.x
(C nesnesindeki alan değeri / işlevi x'e başvurma)
C#x
(C özelliğinde alan türü x referans alınarak)
- işlev imzası (uygulama yok)
- değer düzeyi:
def f(x:X) : Y
- tür düzeyi:
type f[x <: X] <: Y
(buna "tür oluşturucu" denir ve genellikle soyut özellikte görülür)
- işlev uygulaması
- değer düzeyi:
def f(x:X) : Y = x
- tür düzeyi:
type f[x <: X] = x
- şartlılar
- eşitliği kontrol etmek
- değer düzeyi:
a:A == b:B
- tür düzeyi:
implicitly[A =:= B]
- değer düzeyi: Çalışma zamanında bir birim testi aracılığıyla JVM'de gerçekleşir (yani çalışma zamanı hatası yoktur):
- özünde bir iddiadır:
assert(a == b)
- tür düzeyi: Bir yazım denetimi aracılığıyla derleyicide gerçekleşir (yani derleyici hatası yoktur):
- özünde bir tür karşılaştırmasıdır: örneğin
implicitly[A =:= B]
A <:< B
, yalnızca A
bir alt türü ise derlerB
A =:= B
, Yalnızca derler A
bir alt tipi olan B
ve B
bir alt tipi olanA
A <%< B
, ("görüntülenebilir") yalnızca A
şu şekilde görüntülenebilirse derlenir B
(yani A
öğesinden alt türüne örtük bir dönüşüm varsa B
)
- Bir örnek
- daha fazla karşılaştırma operatörü
Türler ve değerler arasında dönüştürme
Örneklerin çoğunda, özellikler aracılığıyla tanımlanan türler genellikle hem soyuttur hem de mühürlenir ve bu nedenle ne doğrudan ne de anonim alt sınıf aracılığıyla somutlaştırılabilir. Bu nedenle, null
bir tür ilgi alanı kullanarak değer düzeyinde hesaplama yaparken yer tutucu değer olarak kullanılması yaygındır :
- ör. önemsediğiniz tür
val x:A = null
neredeA
Tür silme nedeniyle, parametreli türlerin tümü aynı görünür. Ayrıca, (yukarıda belirtildiği gibi) çalıştığınız değerlerin tümü olma eğilimindedir null
ve bu nedenle nesne türünde koşullandırma (örneğin bir eşleşme ifadesi aracılığıyla) etkisizdir.
İşin püf noktası, örtük işlevleri ve değerleri kullanmaktır. Temel durum genellikle örtük bir değerdir ve özyinelemeli durum genellikle örtük bir işlevdir. Nitekim, tür düzeyinde programlama, sonuçların yoğun şekilde kullanılmasını sağlar.
Şu örneği düşünün ( metascala ve apocalisp'ten alınmıştır ):
sealed trait Nat
sealed trait _0 extends Nat
sealed trait Succ[N <: Nat] extends Nat
Burada doğal sayıların bir peano kodlaması var. Yani, negatif olmayan her tam sayı için bir türünüz vardır: 0 için özel bir tür, yani _0
; ve sıfırdan her tam sayı formunun bir türü olan Succ[A]
, A
daha küçük bir tam sayıyı temsil türüdür. Örneğin, 2'yi temsil eden tür: Succ[Succ[_0]]
(sıfırı temsil eden türe iki kez uygulanmış halef).
Daha uygun referans için çeşitli doğal sayıları takma ad verebiliriz. Misal:
type _3 = Succ[Succ[Succ[_0]]]
(Bu, a'yı val
bir işlevin sonucu olarak tanımlamaya çok benzer .)
Şimdi, def toInt[T <: Nat](v : T)
bir argüman değerini alan, türünde kodlanmış doğal sayıyı temsil eden bir tamsayıya v
uyan Nat
ve onu döndüren bir değer düzeyinde işlev tanımlamak istediğimizi varsayalım v
. Örneğin, eğer val x:_3 = null
( null
türün Succ[Succ[Succ[_0]]]
) değerine sahipsek toInt(x)
, geri dönmek isteriz 3
.
Uygulamak toInt
için aşağıdaki sınıftan yararlanacağız:
class TypeToValue[T, VT](value : VT) { def getValue() = value }
Aşağıda görüleceği gibi, orada sınıfından yapılmış bir nesne olacak TypeToValue
her Nat
gelen _0
(örneğin) 'ye kadar _3
, ve karşılık gelen her bir tip (yani değeri temsil depolayacak TypeToValue[_0, Int]
değeri depolayacaktır 0
, TypeToValue[Succ[_0], Int]
değeri depolayacaktır 1
, vs.). Not, TypeToValue
iki türle parametrelendirilir: T
ve VT
. T
değerler atamaya çalıştığımız türe karşılık gelir (örneğimizde Nat
) ve VT
ona atadığımız değerin türüne karşılık gelir (örneğimizde Int
).
Şimdi aşağıdaki iki örtük tanımı yapıyoruz:
implicit val _0ToInt = new TypeToValue[_0, Int](0)
implicit def succToInt[P <: Nat](implicit v : TypeToValue[P, Int]) =
new TypeToValue[Succ[P], Int](1 + v.getValue())
Ve toInt
aşağıdaki gibi uyguluyoruz :
def toInt[T <: Nat](v : T)(implicit ttv : TypeToValue[T, Int]) : Int = ttv.getValue()
Nasıl toInt
çalıştığını anlamak için birkaç girdi üzerinde ne yaptığını düşünelim:
val z:_0 = null
val y:Succ[_0] = null
Çağırdığımızda toInt(z)
, derleyici örtük bir ttv
tür argümanı arar TypeToValue[_0, Int]
(çünkü z
türden olduğu için _0
). Nesneyi bulur, bu nesnenin yöntemini _0ToInt
çağırır getValue
ve geri döner 0
. Unutulmaması gereken önemli nokta, programa hangi nesnenin kullanılacağını belirtmediğimizdir, derleyicinin onu örtük olarak bulmasıdır.
Şimdi bir düşünelim toInt(y)
. Bu sefer, derleyici ttv
türden örtük bir argüman arar TypeToValue[Succ[_0], Int]
(çünkü y
türden Succ[_0]
). succToInt
Uygun tipte ( TypeToValue[Succ[_0], Int]
) bir nesne döndürebilen işlevi bulur ve değerlendirir. Bu işlevin kendisi v
, türden örtük bir argüman ( ) alır TypeToValue[_0, Int]
(yani, TypeToValue
birinci tür parametresinin daha az olduğu yerde Succ[_]
). Derleyici _0ToInt
( toInt(z)
yukarıdaki değerlendirmede yapıldığı gibi) sağlar ve değeri succToInt
olan yeni bir TypeToValue
nesne oluşturur 1
. Yine, derleyicinin tüm bu değerleri örtük olarak sağladığına dikkat etmek önemlidir, çünkü bunlara açıkça erişemiyoruz.
İşini kontrol ediyorum
Tür düzeyindeki hesaplamalarınızın beklediğiniz şeyi yaptığını doğrulamanın birkaç yolu vardır. İşte birkaç yaklaşım. Doğrulamak istediğiniz iki tür yapın A
ve B
eşittir. Ardından aşağıdaki derlemenin yapıldığını kontrol edin:
Equal[A, B]
implicitly[A =:= B]
Alternatif olarak, türü bir değere dönüştürebilir (yukarıda gösterildiği gibi) ve değerlerin çalışma zamanı kontrolünü yapabilirsiniz. Örneğin assert(toInt(a) == toInt(b))
, nerede a
tür A
ve b
türdür B
.
Ek kaynaklar
Mevcut yapıların tam seti , ölçek referans kılavuzunun (pdf) tipler bölümünde bulunabilir .
Adriaan Moors , tip oluşturucular ve ilgili konular hakkında çeşitli akademik makalelere sahiptir ve bunlarla ilgili örneklerden örnekler:
Apocalisp , birçok tür düzeyinde programlama örneğinin ölçeklendirildiği bir blogdur .
ScalaZ , çeşitli tip düzeyinde programlama özelliklerini kullanarak Scala API'yi genişleten işlevsellik sağlayan çok aktif bir projedir. Büyük bir takipçi kitlesi olan çok ilginç bir proje.
MetaScala , doğal sayılar, boole'lar, birimler, HList, vb. İçin meta türleri içeren Scala için tür düzeyinde bir kitaplıktır. Bu, Jesper Nordenberg'in (blogu) bir projesidir .
The Michid (blog) , Scala'da tür düzeyinde programlamanın bazı harika örneklerine sahiptir (diğer yanıttan):
Debasish Ghosh'un (blog) bazı ilgili yayınları da var:
(Bu konuda biraz araştırma yapıyorum ve işte öğrendiklerim. Hala yeniyim, bu yüzden lütfen bu cevaptaki herhangi bir yanlışlığı işaret edin.)