Oprócz HKT Scala oferuje jeszcze inne mechanizmy związane z typami, jak np compound types https://docs.scala-lang.org/tour/compound-types.html (w Scali 3 odpowiednikiem są intersection types http://dotty.epfl.ch/docs/ref[...]types/intersection-types.html ), type members: https://docs.scala-lang.org/tour/abstract-type-members.html (hobbystycznie często używam, w pracy rzadziej). Scala 3 dodaje jeszcze union types http://dotty.epfl.ch/docs/reference/new-types/union-types.html , type lambdas http://dotty.epfl.ch/docs/reference/new-types/type-lambdas.html , match types http://dotty.epfl.ch/docs/reference/new-types/match-types.html itp itd
Co dają .NETowe genericsy? Są reified, więc można robić na nich zaawansowaną ifologię czasu wykonania. Scalowe genericy natomiast są z jednej strony wymazywane (a więc nie da się zrobić zaawansowanej ifologii czasu wykonania), ale są rozbudowane dzięki czemu można zrobić zaawansowaną ifologię czasu kompilacji. Jak dla mnie analiza typów w czasie kompilacji to rozwiązanie lepsze niż analiza typów w czasie wykonania.
Weźmy dla przykładu JavaScript czy TypeScript. Nie ma tam przeciążania metod, ale można to zaemulować za pomocą ifologii czasu wykonania.
https://github.com/snabbdom/s[...]/blob/master/src/package/h.ts
export function h (sel: string): VNode
export function h (sel: string, data: VNodeData | null): VNode
export function h (sel: string, children: VNodeChildren): VNode
export function h (sel: string, data: VNodeData | null, children: VNodeChildren): VNode
export function h (sel: any, b?: any, c?: any): VNode {
var data: VNodeData = {}
var children: any
var text: any
var i: number
// tu zaczyna się ifologia emulująca przeciążanie metod
if (c !== undefined) {
if (b !== null) {
data = b
}
if (is.array(c)) {
children = c
} else if (is.primitive(c)) {
text = c
} else if (c && c.sel) {
children = [c]
}
} else if (b !== undefined && b !== null) {
if (is.array(b)) {
children = b
} else if (is.primitive(b)) {
text = b
} else if (b && b.sel) {
children = [b]
} else { data = b }
}
// tu zakończyła się ifologia emulująca przeciążanie metod
if (children !== undefined) {
for (i = 0; i < children.length; ++i) {
if (is.primitive(children[i])) children[i] = vnode(undefined, undefined, undefined, children[i], undefined)
}
}
if (
sel[0] === 's' && sel[1] === 'v' && sel[2] === 'g' &&
(sel.length === 3 || sel[3] === '.' || sel[3] === '#')
) {
addNS(data, children, sel)
}
return vnode(sel, data, children, text, undefined)
};
Jak widać język nie musi oferować sensownego przeciążania metod by osiągnąć popularność. JS i TS trzymają się bardzo dobrze na rynku. Obstawiam też, że tego typu brak nie jest tematem nieustannych żali. Każdy się dostosowuje do tego co oferuje język. Jeżeli w C# da się rozwiązać jakiś problem orając generyki refleksją czasu wykonania, a w Scali ten sam problem można ogarnąć zaawansowanymi typami i innymi mechanizmami czasu kompilacji to wybiera się rozwiązania dopasowane do języka.
Sporo rzeczy sprowadza się do The Blub Paradox. Jest masa języków, w których można być produktywnym, więc jeśli ktoś osiągnął biegłość w jakimś języku i jest w nim produktywny to na języki bardziej skomplikowane (tzn. mocniejsze w sensie oferowanych mechanizmów) będzie patrzył jak na niepotrzebne udziwnienia.
Inne przykład to receivery w Kotlinie. Czasami na forum Scali zdarzają się ludzie, którzy bardzo mocno lobbują za wstawieniem takiego mechanizmu do Scali. Twierdzą, że taki mechanizm totalnie zmienia ich życie i że Scala musi go mieć by nie być w tyle za Kotlinem. Jak do tej pory nie ma wprost analogicznego mechanizmu w Scali. Są inne, którymi można rozwiązać podobne problemy, ale wprost receiverów w Scali nie ma.