Live data from Hacker News

What was wrong with SML?

blog.plover.com

41–50 of 82 posts

Re: What was wrong with SML?

#41

Earlier quoted context omitted.

Any strict language can implement laziness. The inverse may(?) not be possible.

It's the opposite, no? Basically every lazy language has some way to force evaluation, but in most strict languages laziness is never fully supported. Closest I can think of is a Scheme with continuations, but even then it takes some extra work. I know in CL even primitive laziness takes a lot of work and a code-walker, and whatever it's other flaws Common Lisp is at least adaptable.

I could be totally wrong, happy to be corrected. And I'm less familiar with lazy languages. My very basic impression of implementing laziness is just that you wrap all of your data structures/algorithms in functions that must themselves be called. Like everything is an iterator.

See also: https://en.wikipedia.org/wiki/Lazy_evaluation#Simulating_laz...

Re: What was wrong with SML?

#42
post #38

Earlier quoted context omitted.

I am curious what you use SML for. Any examples? Just interested in seeing what you can do in SML.

Mostly programming languages related: code analysis, compilers, code generators, and formal verification tools. Outside PL work, I’ve used it to build some modeling and simulation tools. I also use it for misc. little tools that I need every so often where I want the type system to help me out. For a brief few years (‘09-‘15) Haskell played this role for me but I went back to SML after getting frustrated with various…

Oh modeling and simulation is an interesting idea, that sounds like a perfect thing to play around with SML for. Thanks!

Re: What was wrong with SML?

#43
post #4

> Scala has a very different solution to this problem, called covariant and contravariant traits. I thought Scala had an even stricter value restriction than ML, where only function/methods may get a polymorphic type?

Yes, that statement is very confused. Scala doesn't do generalization, or polymorphic values, and thus the problem doesn't occur at all.

Re: What was wrong with SML?

#44
post #28

I'm not really convinced by the author's first example. While an element of type bool is an instance of type a, an element of type bool -> bool is not an instance of type a -> a. The issue is precisely an issue of variance, which is mentioned in reference to Scala, but somehow it's glossed over. The type a -> a is covariant in its second argument, but contravariant in its first argument. As a result, you cannot "spec…

SML doesn't have subtyping or variance AFAIK so that can't really explain the issue. The way I like to look at it is to translate to System F. The standard translation would give:

    let m : ∀α. ref (α → α) = Λα. ref [α → α] (λx. x)
    in
      (m [bool]) := not;
      print ((!(m [int])) 23)
    end
This actually has different behavior because the ref allocation is under a big lambda (Λ). Each time it is applied it would generate a new ref cell. So it would generate a `ref (bool → bool)` starting with `λx. x` and assign `not` to it. Then generate a separate `ref (int → int)` (again starting with `λx. x`) and dereference and apply it. Thus this would print `23`.

(The naive model of) SML runs into problems because it erases the big lambdas and so evaluates `Λα. ref [α → α] (λx. x)` to a single ref cell of type `∀α. ref (α → α)`

Sidenote: Variance does have some relation here, I couldn't think of a way to trigger bad behavior without a type that uses the type variable invariantly.

Re: What was wrong with SML?

#45

Earlier quoted context omitted.

Any strict language can implement laziness. The inverse may(?) not be possible.

It's the opposite, no? Basically every lazy language has some way to force evaluation, but in most strict languages laziness is never fully supported. Closest I can think of is a Scheme with continuations, but even then it takes some extra work. I know in CL even primitive laziness takes a lot of work and a code-walker, and whatever it's other flaws Common Lisp is at least adaptable.

In strict languages, you can delay computation by wrapping it in a zero-argument lambda -- i.e., a "thunk." For efficiency, you want to memoize thunks (that's what Haskell does[1]) so that they only ever evaluate once. Scheme has the "delay" operator to create memoized thunks, which you can later "force". It is true that these are not first class in the sense that you need to manually force the computation, but if it were automatic, how would you (efficiently) pass un-evaluated values around?

[1] Incidentally, this is why Haskell's garbage collector needs to be able to deal with mutation. A thunk might graduate to an older generation, and once it is finally evaluated it can end up having pointers to the nursery or other younger generations.

Re: What was wrong with SML?

#46

> In Structure and Interpretation of Computer Programs, Abelson and Sussman describe an arithmetic system in which the arithmetic types form an explicit lattice. Every type comes with a “promotion” function to promote it to a type higher up in the lattice. When values of different types are added, each value is promoted, perhaps repeatedly, until the two values are the same type, which is the lattice join of the two…

Wouldn’t that last example be incorrect mathematically? For example, if the Int8 was -10 and the UInt16 was 10, what would that casting do? Would a better promotion be Int32 for both? Just curious, Julia is a language I’ve been very interested in using for a long time, just haven’t had the opportunity to sit down and learn yet.

Yeah, it's a little iffy. Finding a sensible set of promotion rules is hard. I don't know the reason (this was decided before I was involved), but if I had to guess, it would be because unsigned types are much less common, so if the user has them, they probably want to keep them for a reason.

Re: What was wrong with SML?

#47
post #28

I'm not really convinced by the author's first example. While an element of type bool is an instance of type a, an element of type bool -> bool is not an instance of type a -> a. The issue is precisely an issue of variance, which is mentioned in reference to Scala, but somehow it's glossed over. The type a -> a is covariant in its second argument, but contravariant in its first argument. As a result, you cannot "spec…

The point AFAICT isn't that the type checker sees "bool -> bool" as an instance of "a -> a". Instead, the issue is that during type inference, when the compiler sees "m := not", it will infer that a == bool, i.e. that "m = ref (fn x ⇒ x)" was really "m = ref (fn (x:bool) ⇒ x)".

But then it will forget that inference when type checking "print ((!m) 23)"; there it will infer that a == int, and that the original definition of m was really "m = ref (fn (x:int) ⇒ x)".

Essentially the issue is that type inference forgets that it already solved for "a" and can't assign it to both "int" and "bool".

Re: What was wrong with SML?

#48
I think that in some sense the correct solution to the reference type problem is the Scala solution, which depends on subtyping. This is pretty tricky to reason through, and adding subtyping makes type inference a lot more difficult, so it's hardly surprising that SML avoided this, but it provides a much more satisfying solution. (OCaml later embraced subtyping, but in the particular case of mutable references, it instead adopted the same value-restriction approach as SML.)

Here's the way I understand it; be warned that I'm just starting to understand this stuff, so I might have got something wrong. I'd welcome corrections.

S is a subtype of T (S

    ∀T: T 
which all follow from the above informal definition. Sometimes we make it a lattice, for example by adding a ⊤ type ("top") that everything is a subtype of and a ⊥ type ("bottom") that is a subtype of everything.

In general in the presence of subtyping we can only infer type bounds on most things, not exact types. For example, consider that {3} Given this informal definition, surprisingly, α → α (the type of the identity function) is a "subtype" of bool → bool, not vice versa. That is, α → α α → α is a weird type because it includes an implicit universal quantifier: ∀α: α → α. As it turns out, the judgment above that α → α There's also a function-specific rule for subtyping, and it's a real mindbender: B → C contravariant and their results are covariant. Considering the A We can decompose the operation of taking a reference to x, ref x, into a step of creating a reference r and then applying the reseating operation r := x to it. This operation is valid iff the assignee is of a subtype of the referent type; that is, (r : T ref) := (x : S) is valid iff S supertype of α → α. As mentioned above, this includes bool → bool, but it also includes ℤ → ℤ, (ℤ × ℂ) → (ℤ × ℂ), and the polymorphic type α → α itself. So reseating a reference is contravariant: we can write a value we know is an integer or anything more specific (a subtype such as a positive integer) into a reference we know to hold an integer or anything more general (a supertype such as a real number).

The dereferencing operation !r turns out to instead be covariant: !(r: S ref): T is valid iff S How does this resolve the problem? If m is of type (α → α) ref, ((!m) 23) would work; for example, if m happens to be a reference to the identity function, it evaluates to 23. (In many languages that's the only function of type α → α, in the interests of making the Curry–Howard correspondence meaningful.) But if we've previously seen (m := not), we have an incompatible type bound: (not : bool → bool), so we know that m is of type (bool → bool) ref or a ref to some supertype of bool → bool, U ref where bool → bool not the case that bool → bool This seems to give us a nice, clean solution to the problem of types for references. Aside from being confusing as hell, the cost is that instead of inferring a type for every expression we can now only infer an infinite set of possibilities for its type. This sounds ridiculous but it is precisely what OCaml does for polymorphic variants and objects (though not mutable refs; details are in https://v2.ocaml.org/manual/polymorphism.html). With polymorphic variants:

    # let f a = match a with 1 -> `P (3, 4) | _ -> `Q (5) ;;
    val f : int -> [> `P of int * int | `Q of int ] = 
    # let g b = match b with `P (c, d) -> c + d | `Q (c) -> c ;;
    val g : [ int = 
The "subtype of the type given, like this:

    # `Q 2 ;;
    - : [> `Q of int ] = `Q 2
    # g (`Q 2) ;;
    - : int = 2
But the ">" on f's return type is a "lower bound": you can only use f in a context where its return type is a subtype of the expected type. Including the same type, so we can pass it to g:

    # g (f 1);;
    - : int = 7
    # g (f 2);;
    - : int = 5
Similarly we can write a function that calls a couple of methods on its arguments:

    # let mf o = (o#foo ; o#bar + 1) ;;
    val mf :  -> int = 
Here OCaml infers that the object needs to have a method "bar" returning int and a method "foo" of some type, but the .. makes this an upper-bound object type: it's okay for the object to be of some subtype that has more methods. And similarly if you construct an object with some methods, it can be used in a context that requires some supertype that doesn't have all of them. (There's a very close analogy between an object with a certain set of methods and a function that can be applied to terms with a certain finite set of polymorphic variant tags.)

The subtyping approach would seem to provide a logical solution to the problem of mutability, but it isn't the approach OCaml took, for reasons I don't understand. It's confusing and difficult to understand, but less so than OCaml's current set of rules. I suspect the answer is that OCaml had to be backward-compatible with earlier versions of Caml before subtyping was added, but possibly it's instead to keep the type inference problem decidable or something.

Re: What was wrong with SML?

#49
post #12

Earlier quoted context omitted.

Yup, adoption has nothing to do with being a sensible language. More often it seems to be completely inversely related.

What are some sensible (coherent, load-bearing, force-multiplying etc) tools that have terrible adoption? Unreasonably open-ended question (suppose the scope is ML, or perhaps FP in general, or maybe even wider) - but I'm very curious.

Erlang is underutilized at least in part due to its odd syntax, despite being a powerful tool. Elixir is helping address that, although I much prefer Erlang’s syntax.

Re: What was wrong with SML?

#50

> In Structure and Interpretation of Computer Programs, Abelson and Sussman describe an arithmetic system in which the arithmetic types form an explicit lattice. Every type comes with a “promotion” function to promote it to a type higher up in the lattice. When values of different types are added, each value is promoted, perhaps repeatedly, until the two values are the same type, which is the lattice join of the two…

the main thing Julia loses from this approach is function types. since each function is a user extendable blob of methods, there basically ceases to be a meaningful notion of the type of a function. imo, this is a worthwhile trade-off, but it can sometimes be annoying (and it's why Julia is hard to compile ahead of time. this pattern makes figuring out which methods you need to compile Turing complete)
Post reply on HN