For instance, if A < B (B extends A), What is the relationship betwern Array[A] and Array[B]?
If you are just reading the array, you would want Array[A] < Array[B]. If you are writing to the array, you would want Array[B]<Array[A]. If you are doing both, you want Array[A] to have no relation to Array[B].
This problem doesn't come up in ML style languages because they do not make use of inheritence.
He's right. It's easy to understand one level of variance: you can replace a return type by a subtype and a parameter type by a supertype. (I wouldn't be surprised that many programmer don't undestand this.)
Two levels already requires some deep thinking (assuming definition-site variance: `List<Object>` or `List<String>` as a return type / parameter type, was is allowed to replace it?
More than that? (`List<List<String>>`) Hahaha, good luck.
And by the way, Java has notations to specify the variance of type, but only at the use-site, which is different from doing it at the definition site (both enable expressing things the other can't do... but you can actually have both, as I think is the case in Kotlin, though there are some limitations).
Nevertheless, I have a point because:
1. Things get harder with contravariance. 2. Things get harder when you mix covariance and contravariance.
The prototypical covariance class is Producer<T> (with method produce() returning T) while for contravariance that's Consumer<T> (with method consume taking a T as parameter).
Assume each class has a superclass (Consumer0<T> and Producer0<T>) and a subclass (Consumer2<T> and Producer2<T>). Assume V extends U extends T.
Can you list the subclasses and superclasses of Consumer<Producer<U>>?
Personally, I have to think carefully about this for a minute or so, and I've been there before a couple times.
This is not an especially complex scenario either. I've seen things get worse in practice.
Which of course doesn't change the fact that imperative languages trying to combine generics + inheritance + mutability are in for a world of hurt.
Except for OCaml and Scala (or any other ML supporting subtyping), where you could simply define type's variance.
def writeFirst: (xs: Array[B], x:B): xs[0]=x;
var as: Array[A] = ???
var b:B = ???
var a:A = ???
a=b; //This is fine, since B extends A.
as[0]=a //Obviously fine, since a:A
as[0]=b //This better be fine, otherwise the above 2 lines just punched a hole in our type system.
writeFirst(as,b); //If I am allowed to do the above, I should be allowed to do this.
For completeness, the opposite example: def getFirst(xs: Array[B]):B = xs[0];
var as:Array[A]
var b:B = as[0] //This shouldn't work. Not all A's are B's
var b:B = getFirst(as) //Simmilarly, this shouldn't work.I must be missing something.
The core meaning of A < B is the is-a relationship. That is to say, a value of type B is also a value of type A.
I assume you agree agree that my first example ought to type-check (although, as the second example shows, there is an argument to be made that it shouldn't). The question is how does it typecheck?
In the last line, we call writeFirst(as,b). Here, the first parameter has type List[A].
However, writeFirst is declared as taking a first parameter of type List[B]. The fact that this works means that List[A] is-a List[B], which is the defining feature of List[B] < List[A].
If this were not the case, then we would have needed to define writeFirst with a type along the lines of:
writeFirst[X < B]( xs:List[X], x:B): xs[0] = x
Where we explictly declare that the type parameter of the list is a subtype of B. In this case List[A] could be used not because of the languages decision on variance, but because the program explicitly typechecks with X=A.
Note that this actually changes the return type. In my original example writeFirst(as,b) would have a natural return type of List[B]. However, in this new example, the natural return type would be List[A].
This second example is closer to how ML style languages work.
EDIT: It occurs to me that I was thinking a bit too functional in this. The natural return type of writeFirst is void in (most?) OOP langauges because arrays are mutable. What I wrote assumed that the natural meaning of writeFirst was to construct a new list which replaces the first element.
One other factor is that functional languages typically are theory first, implementation second, while non-functional languages tend to more often have the language features follow whatever the implementation admits. I know for Go they fretted a lot about how you'd efficiently compile generics, which is not something that comes up when you're designing System F or whatever.
But I believe that subtyping has a massive impact on generics, particularly in OOP languages where people expect to use lots of subtypes. There are all of these new questions around not only bounded polymorphism but also variance and mutability and how inference works.
Even TypeScript, which is following a lot of the design decisions already established by C# with the same person behind both, is still kinda just meandering around the design space and continuing to make changes to the semantics in new versions.
A super awesome type system is worthless without the basic requirements for scalable software development.
Ocaml is billed as the pragmatic ML, but the syntax really sucks and nominally typed structs aren't nearly as good. They also have 3 competing standard libraries.
Haskell is too ivory tower. Most devs simply can't be bothered.
StandardML is that awesome middle. The language choices are pragmatic compared to haskell (mutable refs, side effects, and not lazy). The syntax is super simple and consistent (unlike ocaml). The concurrent ML extensions offer good multi-threading (still waiting on ocaml). The mlton compiler is very fast. There's only one standard library and it's decent. The big thing holding the language back is third-party libraries. If Google threw their millions at SML instead of go, SML really would be better in every way.
OCaml structs are structurally typed.
> They also have 3 competing standard libraries.
There is only one standard library, stdlib.
He is talking about SML here.
Care to elaborate, what Go has to do with Core or Batteries? Both are mostly container libraries, and you don't need them since most of the useful staff is in stdlib.
Check revdeps in OCaml, nobody uses batteries, and nearly nobody safe JS uses Core.
XML parsers, Lwt, servers are in separate packages, which is the only and right thing to do.
>then for async do you choose Async or Lwt
Lwt, it's a non-question. Async is for JaneStreet only.
> And as said the build system (when you're used to Go) is poor.
Dune [1] is far superior to Go's build system. Extremely fast, composable, supporting packaging and os-dependent configurations, extremely easy to config. No abomination like "import github.com/package" or "//go:" in your code.
Go build system doesn't have even a fraction of nice features dune have. For example I could `dune utop ./path/to/my/libs` to build my libs and run a nice repl to test them.
Go doesn't even let you to configure your warnings precisely.
You can read about Ocaml's nominal record typing in the [ReasonML docs](https://reasonml.github.io/docs/en/record) (it's more clear there IMO).
SML gets this right in my opinion. If I create a record `{foo = "abc", bar = 123}`, I can pass that record on to ANY function that needs a record that looks like {foo:string, bar:int} fields because it looks at the structure rather than the type of the record constructor.
Another nice property of the SML approach is that you don't need named arguments. Instead, pass a tuple with names (aka a record). One set of syntax rules covers both cases. I also rather like the ability to access record fields with the hash syntax (eg, `#foo myrecord`) when I don't want to destructure.
You are confusing records with structures, it seems. Structures exist in module language. Records are nominal in SML as well. Access functions for tuples and records are a dirty ugly hack build-in for convenience, they have nothing to do with structural typing, they are inferred in place.
OCaml has structural typed records, they are called objects.
let obj = object method pi = 3.14; method name = "Pi" end
val obj : < pi : float ; name : string >
is structurally typed record.> I can pass that record on to ANY function
No, you can't.
#foo { foo = 42 }
works, but fun f x = #foo x
doesn't. It's an ugly hack, typing is still nominal. A hack, just like SML's arithmetic op overload.Compare it to OCaml, which have proper structural typing for objects and raw polymorphism
let f x = x#foo
f : < foo : 'a; .. > -> 'a
Edit: sorry, indeed typing is structural since you can write `{x:typ} -> typ`.Anyways OCaml have structural typed records and raw polymorphism and subtyping support for them.
> Another nice property of the SML approach is that you don't need named arguments. Instead, pass a tuple with names (aka a record).
What's your point? You can use records as arguments in OCaml as well.
F# seems to meet all of that except that native static compilation isn't the default (but is available).
Define sane syntax. C-like abomination? At least unlike C it's unambiguous.
Sure, I'm asking for actual traits, which makes something easily or hardly parsed. The only languages that I find more readable than OCaml are Ada, Pascal and SML. What causes mental overhead in OCaml/SML syntax for you?
Do you find
func test (f func(a int, b int) int, x int) int
more readable than val test : (int -> int -> int) -> int
Maybe, you've just got used to it?1. Familiarity. Like (presumably) most programmers, I'm familiar with languages in the C syntax family. Of course you can protest that this is a subjective criteria, but little good that will do you as you try to convince your colleagues to adopt (what they perceive to be) your pointlessly cryptic language for the next project.
2. Visual structure is important and while OCaml's minimalism makes for elegant parser algorithms, it works against human psychology (or so I strongly suspect).
Look, I want to like OCaml/SML. I think the type system is a step in the right direction, but the type system is just gravy and the practical concerns--the fundamentals--are neglected (as much as you may protest to the contrary).
Could you show an example?
> it works against human psychology
What minimalism are you talking about? It uses nearly the same notations as mathematicians used for decades. Most of the constructs are very like to those in Python.
It's not, it's an essential, basic feature.
void lol<A, <B, C<D>>, E>() for (int i = 0; i < 10; i++) {}
to ten random people without prior programming experience and see how many of them can correctly tell you what all that means. Similarly, { a, b in a > b }
is probably not very clear to people who don't write Swift and perfectly lovely closure syntax to people who do.There's language syntax that's actually unreadable, for instance due to using names that obscure or otherwise don't clearly express what a construct is/does or using the same operator/keyword for too many different context-dependent purposes. I don't quite think the sheer presence of angle brackets makes code unreadable, any more than the sheer presence of curly braces or parentheses do.
All features can be abused to make the code unreadable. E.g. all modern languages have regular expressions in their standard libraries, despite complex regular expressions are essentially write-only code.
Substitute "generics" for pretty much any bit of programming syntax, and this statement is still true.
template<> void lol<A, <B, C<D>>, E>()
I'm also not sure why in OOP-land, generics are this crazy experimental weird feature, when in functional languages, people figured out how to implement parametric polymorphism (the original term for generics) in quite reasonable ways. I get that subtyping adds some complexity, but overall I don't understand why such a basic way to build abstractions is so controversial in (some) OOP languages. If anyone has some context on why this is more difficult to have in Java-like languages, I'd be curious to hear it.