Understanding the Elm type system
adamwaselnuk.com
adamwaselnuk.com
This beautifully explains why types are so useful. I don't know if the number of bugs goes down, but it sure is useful to have an assistant check all the implications of a change I want to make, and does that in a second.
Of course Flow/Typescript's approach to typing isn't as complete as Elm's, but you can still gain some of the benefits by using one of them; and for many of us, introducing Flow/TS at work is a lot more likely to happen than introducing Elm.
Mixing subtyping with subsumption and parametric polymorphism (type parameters) is really very difficult though. From a theoretic point of view, type inference and type checking are harder. From an usability perspective, the programmer now has to deal with variance annotations (for example, in Java `class List[+T]` means the T parameter is covariant) and bounded polymorphism (for example, things like <? extends T> in Java).
After working on a startup that had a server architecture similar to AOL Server and also seeing some heavy Zope deployments (late 90's), I never got to understand the use of dynamic languages for large codebases.
Dylan?
Honestly asking - I used the language a bit but I didn't read anything on its implementation(s).
So in the end you end up with code that looks no different than from static languages.
With ML-like type inference and dynamic type support, there is hardly any disadvantage with static type languages and you get the tooling support.
Dynamic languages can have very nice tooling, and we are yet to regain what was possible in Xerox and Genera environments, but your environment needs a live image of the code to produce good results, hence JIT.
Even though I keep getting bit by bugs that a proper type checker would catch (Dialyzer is not sufficient for some of these bugs unfortunately) the problem is such a perfect use case for the BEAM VM that I don't really have a lot of choice without increasing the complexity by a lot.
I very much agree with the author; learning functional programming idioms have had a profound impact on my ability to model problems in code, regardless of the language I'm using.
I admire Elm so much for putting an emphasis on the user experience, recognizing that it has been one of the biggest blockers to making functional programming mainstream.
[0] Basics: http://learnyouahaskell.com/types-and-typeclasses
The documentation looks very accessible, there's a bit of introduction to the language, and then hands-on, that's nice.
edit: by documentation I meant this gitbook: https://www.gitbook.com/book/evancz/an-introduction-to-elm/d..., but the official documentation looks similar.
(0) statically and strongly typed, with type inference
(1) algebraic data types with pattern matching and exhaustiveness checking
(2) purely functional
So the way it interacts with the dom seems to be similar to what I've read about React, in that it seems to always map the model to a view, rather than messing around with existing dom elements. That seems like something that is easy to understand and to reason about.
The downside seems to be that it comes with quite a bit of js size, slightly larger than jQuery, if I understand it correctly.
When I was gathering information about typed languages that compile to JS, I found Elm rather nice, but calling JS libs was rather cumbersome. Somewhere I read that PureScript has a better integration with JS, so I thought it was a "thinner" layer :D
Also, Purescript's FFI is really easy to use (unlike Elm's), afaik this was one of its original design goals.
This is not snark. Why Koka when ML and Haskell exist? What does it bring to the table that is new? What Haskell calls monads Koka calls effects? But Haskell has lazy evaluation, right -- and Koka is strict (like ML) and tracks effects (like Haskell). Does Koka complete this reductive functional programming matrix?
/ Monads / No monads†
-----------------------
S | Koka | ML
L | Haskell | Miranda
† No monads == Nomads?Also, the way Haskell handles composing effects (monad transformers), while precise, sometimes leaves a lot to be desired in terms of usability. That's why other approaches to managing effectful computations are being explored, like algebraic effects.
row Fields = [name: String, age: Int]
type Person = MkRecord Fields
Which have kinds: MkRecord :: Row Type -> Type
Fields :: Row Type
Person :: Type
You can construct variant types in a similar way: row Cases = [success: SyntaxTree, failure: ParseError]
type ParseResult = MkVariant Cases
Which have kinds: MkVariant :: Row Type -> Type
Cases :: Row Type
ParseResult :: Type
Rows may contain things other than proper types, for example: row Effects = [foo: State s, bar: Except e]
type Action = MkFunction Effects
Which have kind: -- function types now take three arguments:
-- - row of effects
-- - argument type
-- - return type
MkFunction :: Row Effect -> Type -> Type -> Type
Effects :: Row Effect
Action :: Type -> Type -> Type row = sequence
record = named conjunctive sequence
variant = named disjunctive sequence
:)Can you explain what you mean when you say "proper" types? (So I can get what you mean from there on out.) Are all your examples using regular standard Haskell syntax? (I get that you are making up type (and kind) names for illustration purposes). Thanks!
That's news to me. Could you post a proof?
I stumbled on Elm looking for a good language (Haskell) to build a new language. Try compiling Elm. hint: it's written in Haskell and if you squint, the type system is a simpler derivation of Haskell.
I think I am now officially feeling old. A language I never heard of is the introduction to programmers to static typing??
I started with C instead of SML, which was available then, I think, because everyone I knew coded in C and they all said it's ok and a good language to learn.
The same is probably true today: you first need to know that a language exists before trying to learn it, and beginner programmers are going to only know about languages their peers (other beginners, probably) use.
Since the "strong" vs "weak" axis of type systems is already not extremely useful, as very few languages are truly weakly typed, it's usually fine.
(0) Algebraic data types, pattern matching and exhaustiveness checking rule out forgetting to handle all the qualitatively different possible outcomes of an operation.
(1) Statically typed effects rule out sneaking effects into computations without making them knowable to the user.
(2) Substructural type systems rule out resource leaks and uses after free.
(3) Rust's borrow checker rules out concurrent modifications of the same object in memory.
How usefully typed are C and Java?
connectWords : String -> String -> String
This is one of the most maddening things for me about Haskell-like languages. I can never remember if -> is left or right associative. I mean there is only one way that makes sense: # String -> (String -> String)
But it could also be # (String -> String) -> String
Of course you get used to it after a while, but a nagging feeling remains. I would really prefer a bit of syntactic sugar.> The type annotation for connectWords is telling us that connectWords is a function that accepts two strings and returns a string.
Or maybe I misunderstand this whole thing. I know nothing about Haskell/Elm.
So in order to construct functions that accept more than one argument, you actually return successive functions that apply successive arguments, known as currying.
connectWords : String -> String -> String
connectWords firstWord secondWord =
firstWord ++ secondWord let prefix = connectWords "Hello "
world = prefix "world"
bob = prefix "bob"
in ...
`world` is "Hello world", and `bob` is "Hello bob". That is the power of currying. A maybe more useful example is specifying the mapping function in `List.map` without supplying the list to map over. This allows you to use the same map with multiple lists. connectWords = \ firstWord -> \ secondWord -> firstWord ++ secondWordDoes this kind of 1/arity have performance implications for Haskell? Does the compiler lower (or is it rise? inline?) the functions to produce efficient machine code?
f: Int -> Int -> Int
f a b = a + b
the compiler (at least in Haskell) creates f as a function that takes a single argument, a, and outputs a function. That outputted function takes a single argument, b and outputs an Int.When you apply the function f:
f 5 7
what's really happening here is that f is first applied to 5. This creates a new function, call it g, taking in a single integer and outputting that integer plus 5. Then, this new function is applied to 7. (The compiler may optimize a lot of this away, but that is how you should think of what's happening in my opinion.)Hope that helps!
f: Int -> (Int -> Int)
...
(f 5) 7For example of a typical imperative approach to applying functions:
function price(product) {
product == 'book' && return 20
product == 'laptop' && return 10
}
function addShipping(product, price) {
product == 'book' && return price + 10
product == 'laptop' && return price + 5
}
function addTax(price) {
return price + (x * 0.13)
}
function total(product) {
cost = price(product)
subtotal = addShipping(product, cost)
total = addTax(subtotal)
return total
}
Compared to a haskell-style JS which combines currying and function composition (. combines functions in Haskell): price :: Product -> (Int)
price p =
p == 'book' && () => 10
p == 'laptop' && () => 20
shipping :: Product -> (Int -> Int)
shipping p =
p == 'book' && (x) => x + 10
p == 'laptop' && (x) => x + 25
tax :: Int -> Int
tax x = x + (x * 0.13)
total :: Product -> Int
total p = tax . shipping(p) . price(p)
Alternatively, you can easily create object specific total functions: totalBook = tax . shipping('book') . price('book')
totalLaptop = tax . shipping('laptop') . price('laptop')
Note how in the FP `total` version the data is not held in temporary variables but passed directly to the next function, which could be a performance gain.Another benefit is how it's easier to work with curried functions when using map/reduce and list comprehensions - two other fundamental building blocks of FP programs. For example, you can pass a curried `shipping` function directly to map whereas `addShipping` would required an anonymous function (since it has two arguments).
map(shipping('laptop'), [5, 10, 20, 30])
vs
map(function(price) {
addShipping('laptop', price)
}, [5, 10, 20, 30])
The performance question is largely a question of the implementation and compiler optimizations. But considering we're working in a browser environment the "bottle-neck" is going to be DOM interaction not passing around curried functions everywhere.Additionally, you are much more like to create functions in FP with single arguments rather than multiple in order for composition to work cleanly.
(I looked at the pics and saw `List.map` everywhere.)
Learn You an Elm examples don't compile on the try-it-live console.
Official examples do, but fail to compile on a local install (after setting up elm package install etc).
Any suggestions on where to start troubleshooting?
It's failing to find modules that are in the core standard lib.
Maybe my environment isn't set up correctly?
I tried the first one (the counter). Copy/pasted directly into a dir as 'counter.elm'. Initialized the dir with 'elm package install' to get the core library.
$ elm make counter.elm
I cannot find module 'Html'.
Module 'Main' is trying to import it.
Potential problems could be:
* Misspelled the module name
* Need to add a source directory or new dependency to elm-package.json
Edit/update: source-directories is ["."] in elm-package.json. Dependency "elm-lang/core": "4.0.1 <= v < 5.0.0" is in elm-package.json. elm-version is "0.17.0 <= v < 0.18.0". elm-make is elm-make 0.17 (Elm Platform 0.17.0).core is installed in elm-stuff/packages:
macbook:core me$ pwd
/Users/me/elm-learning/elm-stuff/packages/elm-lang/core
additionally, macbook:elm-learning me$ which elm
/usr/local/bin/elm
Edit2: so, I then guessed that Html wasn't a part of the core lib, and tried to install evancz/html, but apparently that doesn't work with 0.17 (I get an error about version constraints). I'm guessing the elm-lang.org editor doesn't use 0.17?Kinda amateur hour with leaving that out of the guide. I wonder how many others have gotten as frustrated and stopped trying to learn it!
I'll submit a PR to update the docs in a lil bit.
Is this possible to do in imperative languages? i.e. in C++ or Java? If not, why?