Hacker News new | ask | show | jobs
by higherkinded 2504 days ago
Off the topic but,

Why keep reinventing the wheel when there's an ML language family already? Why do people keep giving up these juicy Hindley—Milner-ish type systems and these brief and concise equations for anything? It just doesn't make sense for me.

4 comments

In both SML and OCaml writing a function that's polymorphic over numeric types is a huge pain in the ass, so I'm not sure it's a good choice for numeric programming. (Unless you count Haskell as a member of the ML family?) Though it could be that generics aren't that important here, and I'm just a weirdo who liked to do things like implementing a dual number type to get forward-mode automatic differentiation of standard functions for free...
> In both SML and OCaml writing a function that's polymorphic over numeric types is a huge pain in the ass, so I'm not sure it's a good choice for numeric programming.

What? In numeric programming we usually want the opposite, to write a function for specific type, and to make it run fast.

Also, in OCaml you can simply make your computation a parametric module of some algebraic structure, something like

    module type Ring = sig
       type t
       val zero : t
       val one : t
       val (+) : t -> t -> t
       ...
    end

    module Numeric_staff (N : Ring) = struct
       ...
And have a completely static numeric computation without any dynamic dispatch. That's resembling C++'s templates.

You also can use classes if you want dynamic dispatch

    let (+) x y = x#add y
Haskell is its direct descendant, so of course it counts. It has all the traits required to be called that, and I'm implicitly talking towards it in the initial comment while still respecting the family. And for what I know, OCaml has generics, so I'm not exactly sure what do you mean here.
I haven't ever written any OCaml, but I thought it has separate operators for ints and floats. I've written some SML and it has some special hacks to allow overloading on built-in operators. While I do understand that ML style modules are theoretically more powerful than Haskell's typeclasses, they always struck me as much clunkier for everyday use (that's why I personally consider ML family and Haskell family as separate - the approach to ad-hoc polymorphism is an important difference in my opinion)
> they always struck me as much clunkier for everyday use

They are simply explicit. What's so clunky about modular implicits, for example?

Personally, I prefer explicit modules and hate typeclasses, because I can't look at the code and say if this operation is a primitive operator or some dynamically dispatched class method.

Besides, modules are way more powerful, and have a way broader use. They can encapsulate state, types, can be parametric etc.

Each language will have different design priorities. For example auto-currying is great but it wouldn't make sense in a multiple dispatch language since you have to evaluate every argument to dispatch (which makes n-arity functions necessarily first class, not a composition of 1-arity functions). Another example is that Julia's type system exists to make it more dynamic (while maintaining high performance), while ML languages focus on static properties.

But previous designs are great for inspiration (like you say, Julia has a lot from them, like sum and product types, subtyping, parametric polymorphism) in the same way it also borrows from Lisp (CLOS). You can probably find in the literature/issues/discussion if you're curious how the language ended up this way, for example:

https://arxiv.org/abs/1808.03370

> auto-currying is great but it wouldn't make sense in a multiple dispatch language since you have to evaluate every argument to dispatch (which makes n-arity functions necessarily first class, not a composition of 1-arity functions).

I have a question on this. I'll note up front that this is not an area of expertise for me.

It seems like knowledge of the types is/could be embedded in the process of currying. Multiple dispatch of a 2-ary function depends on the type of arg1 and arg2. If I call this with just the first arg, then the curried function "has knowledge" of the type of arg1. So now I have a 1-ary function that can do single dispatch on the type of its argument.

This seems possible to me to inductively extend to n-ary functions, since the process of consuming one argument by currying embeds knowledge of the type of that arg in the newly created function of n-1 arity.

Am I way off base here?

I'm very far from an expert as well, but as I see you could have automatic currying in the same way I can write in Julia x -> f(x, 10) (manual currying using anonymous function) but automatically for every function. But the language would have to disallow using them for dispatch purposes:

f :: (Int -> Double) -> Int -> Double

That would have to dispatch to

f(::Function(::Int, ::Double), ::Int)::Double

Now the dispatch will have to be recursive (it will have to apply to each argument and arguments arguments to evaluate which is more specialized), making finding the most specialized version of a function even more chaotic. I'm trying not to think what dispatching on return types means for the compiler though (and that's not necessary for the auto currying).

And because of the currying, if you apply the first argument you'd have a (g :: Int -> Double) which has the memory of receiving (Int -> Double). But now trying to dispatch on another function with that partial will it dispatch as if it were a normal Int -> Double or will the "knowledge" of receiving a previous argument change which method it will pick?

To be honest I have no idea, it's probably possible but it feels like it would be something very different from Julia.

>which makes n-arity functions necessarily first-class

Excuse me but tuples. You still have to observe them to evaluate them but that's all you need to simulate n-arity in the way you want.

>more dynamic

Well, Hindley—Milner is how you do type-safe stuff with inference that annotates for you. For any generic purposes you just put a typevar with constraints, so I don't see a problem here and can't really get what you gain. MLs are more about proper composition.

Do you know of an ML language that's widely used in scientific computing? Julia was made to compete with R, Python, C++ and Fortran, combing high-performance with ease of use and general computing.
That is exactly the thing that baffles me most. Procedural languages that are inherently distanced from math are used for math for whatever reason. That's basically what I've said in earlier comment.
Mathematica and Maxima are much more functional, for what is worth. Mathematica is pretty much a redressed Lisp (much lispier than Julia), and Maxima is not even redressed (it uses s-expressions).

>Procedural languages that are inherently distanced from math are used for math for whatever reason.

I think that historically it wasn't easy to make an efficient functional language. Numerical languages all trace back to Fortran after all (which is still alive and well). Nowadays, I think it's mostly because of inertia, and researchers having better things to do than learning a completely different programming language.

In Julia, the math part is mostly functional. Structs/Data are immutable by default (which cover most basic numeric types) and while arrays are mutable it's very common to write operations in the form of broadcasting (vectorized math) which will not mutate (and you have all higher order functions). In general most languages are this way to some extent (I don't know languages with mutable base numbers or math operators, outside of stuff like +=).

But mutability is also a great tool to have, for example for efficient dataframe handling and neural network weights.

Julia type system is based on subtyping.
Ok but I'm talking of ones based on composition.