Hacker News new | ask | show | jobs
by ccapndave 3689 days ago
I moved from React/Redux/Typescript to Elm a few months ago, and I'm now on my second serious project with it, making mobile application with Cordova. I've found it an absolute pleasure to use, and I especially like that you can be pragmatic about it; if there is something that is annoying to do in Elm then you can simply drop into Javascript/Typescript using ports.

Coming from languages like Actionscript/Javascript/Typescript/PHP I have found the whole Elm experience quite mindblowing; if it compiles, then it just seems to work. I hardly ever find myself poring over bits of code trying to debug a subtle error, and if I do the problem is going to be in the Javascript/Typescript bit.

Basically, I'm sold :)

7 comments

> if it compiles, then it just seems to work

That's exactly been my experience with Elm.

For example, on a web project I had been working on I decided to add the 'search' feature; the very first change[1] actually worked! This is generally not possible with JavaScript.

[1] https://github.com/srid/chronicle/commit/4a6c18147bf1596b234...

Whenever I write a bunch of code that works right away I'm highly suspicious that I'm missing something.
>> if it compiles, then it just seems to work

This. I spent the first 14 years of my career in statically typed languages and only this year began working professionally on a Rails app... I really miss the compiler.

Rust has some really good compile-time checking, it really makes you express everything in a completely non-ambiguous way.

C++ on the other hand compiles stuff that may or may not work at all, it doesn't care, which can lead to all sorts of undefined behaviour down the road.

Compilers are not all created equal.

Huh. I find that when my C++ compiles, it almost always works. Correctly.

But I used the language for nearly 20 years, so maybe it's experience and the patterns I've learned?

Not that Rust or Elm aren't better options; don't know, really. Despite feeling like I'm an expert in C++, I don't actually like the language much. I've used Go some, and I like it, but I haven't tried Rust or Elm yet.

Well, I find that when my C++ compiles <segmentation fault>

When I first learned Java, still in my undergrad, my first impression (and nearly everybody's) was that when it compiles it works. It was still more work than Perl, Lisp, Prolog, and everything, but didn't require the annoying C debugging cycle.

But, anyway, all that is kids play near Haskell.

Hello! I don't know if English is your first language or not, but that last sentence was significantly confusing that I wanted to correct it for you and others who may read it.

The two idioms are 'child's play' & 'next to'. The words are correct synonyms but the usage is unusual enough that I read as having a totally different meaning my first time.

I was also confused by it. However I read the last sentence as the author making a meta-point by using purposefully incorrect grammar, a sort of reversal of the first sentence about c++ code compiling but leading to segfault--like an artistic statement. If it was an accident, then all the better!
>didn't require the annoying C debugging cycle

Are we talking C or C++ ? C compiler is basically useless for compile time error checking because C type system is ridiculously weak. C++ on the other hand can buy you a lot of things with templates, richer type semantics, etc.

And also keep in mind that C++11 and onward is very different from C++ of yore and it catches a lot of errors at the compile time - move semantics and RAII really buy you a lot - the downside is that it's opt-in so the compiler won't force you - and it's less strict than say Rust, but it still gets you there 90% of the way.

Both languages have approximately the same problems for debugging. Explicit memory references are hard to follow, lack of overflowing protection requires a lot of extra caution to catch the errors, and failures lead to an unforgiving stop with at most a core dump. C++ is much better for statically catching errors, but templates and copy/move semantics make it even harder to debug.

And yes, I imagine C++11 is much better. Unfortunately, I didn't write anything big in C++ since then.

You really owe it to yourself to give Rust a shot. A lot of things that you've encoded in convention(as any good C++ programmer should) are right there at the language level.

Ownership? You get an awesome sliding scale of Borrow Checker <-> Boxed <-> Rc/Arc

Mutation? Covered in Copy+Clone/Cell <-> &/&mut <-> RefCell

Thread Safety? Sync + Send guarantee that things which need to stay on a specific thread cannot be shared.

Combine that with Sum Types, Pattern Matching and much stronger functional constructs makes for a very compelling language.

Rust does sound interesting, but it doesn't seem to be focused on the areas that I need right now.

Go has GoRoutines, which are light threads; they can be executed on multiple CPU threads, but by default Go only allocates one CPU thread per physical CPU, even if you allocate tens of thousands of GoRoutines.

For the kind of networking server code I'm writing, light threading is far more efficient than using an OS thread per connection. 25-50x faster at higher server loads.

That's why I've ignored Rust to date; if Rust has light threading built in, but no one is talking about it (haven't found anything but real threads in my quick Google searches), then I'll take a look. Otherwise it will need to wait for me to have a task that Rust would be good for, and I'll stick with Go and TypeScript for the problems I'm solving right now.

Ah, sounds like Erlang/Elixir would be a better fit since the BEAM is the king of lightweight threads/processes.

You said C++ so that's why I suggested Rust, generally a GC tends to preclude spaces where C++ excels which is why I would think you'd want to try something other than Go.

On the Rust side there's things like mio for fast io but coroutine/light threads aren't really a focus afaik. I would still think you could get solid performance (and much better memory use, see Dropbox's replacement of Go) with an event based Rust system although that's not really my area of expertise :).

> But I used the language for nearly 20 years, so maybe it's experience and the patterns I've learned?

It definitely is. This is not the usual C++ experience. You are probably sidestepping all sorts of undefined behavior due to experience alone.

This matches my experience with C++. My code has improved hugely in simplicity and robustness as I've learned which C++ features are productive and which are just minefields.
> Huh. I find that when my C++ compiles, it almost always works. Correctly. > > But I used the language for nearly 20 years, so maybe it's experience and the patterns I've learned?

By contrast, I learnt OCaml and Haskell 2 weeks ago, and when my code in those languages compiles, it always works. I expect I'll have the same experience with Rust.

Funny. I managed to get Haskell to crash within about 20 minutes of first trying it out.

OCaml is supposed to be pretty fast, but so far Haskell hasn't impressed me with performance either.

What code did you write?
It is a bug in the compiler. (Old joke).
I thought it was a bug in the specification? (Older joke.)
I have a marvelous proof that this bug does not exist, but the heap is too small to contain it.
The new C++ Core Guidelines & Guidelines Support Library and related tools are interesting efforts to specifically solve those problems.
If the issue is that "C++ compiles stuff that may or may not work at all" then I don't see how those guidelines will help the situation. A C++ project may or may not follow those guidelines and then we're back where we started.
My understanding is that the tools are the answer - it's a linter that must be run before landing/merging. See https://youtu.be/hEx5DNLWGgA for a demo of such tools.
Tools are just band-aids over an unsafe type system, and they are all too easy to ignore or never use them in the first place.
Give it another year and the new batch of hotshot developers will reinvent static typing and perhaps one or two other features from the 1970s, but it'll have some cool name and only implement 50% of the functionality.
Modern static typing isn't remotely related to typing from the "1970's"
Well, depends on what you mean with modern static typing. In some ways, mainstream programming is finally catching up with ML, which is from the 1970s.

If you mean cutting edge PL research it's a different story of course.

The type systems in modern languages like Swift, Rust, Scala, Haskell, and even Kotlin go way beyond classical Hindley-Milner. Classical Hindley-Milner gives you parametric polymorphism over algebraic data types, along with an inference algorithm. No subtyping, no casts, no coercions, no overloading, no polymorphic literals, no interfaces. You basically get SML.

Consider that in 2006, MPTCs with fundeps was the preferred way to represent "a collection of items that can be compared via equality" [1]. Associated types were a vague research idea [2] that showed a lot of promise, but hadn't been implemented in any language with type inference (they did exist in C++ templates, but C++ isn't exactly what people think of when you say "modern type system"). Now both Rust and Swift feature polished implementations of associated types and use them pervasively in the standard library.

[1] https://en.wikibooks.org/wiki/Haskell/Advanced_type_classes#...

[2] https://prime.haskell.org/wiki/AssociatedTypes

> You basically get SML.

SML is still more usable than the statically type languages in use the most today (C++ and Java).

"This will be the sixth time we have reinvented basic programming, and we have become exceedingly efficient at it."
What is that a reference to?
The Architect's big speech in The Matrix Reloaded, where he's talking about the machines tearing down and restarting the Matrix. "But, rest assured, this will be the sixth time we have destroyed it, and we have become exceedingly efficient at it." It was from memory and so a bit munged, though.
You might be interested in the Crystal language. It's a statically-typed, Object-oriented, Ruby inspired language. It's Ruby with types, basically.

You might also be interested in Elixir which ties in to the Erlang ecosystem, but if you are super into OOP (like me) you might find it slightly more difficult, but since you have experience with static-types, you probably know some FP stuff, and so it's not that bad.

Either way, best of luck. It takes a brave soul to fight with Rails. I've been writing Ruby for about three years now and still haven't touched Rails just because I can imagine how little fun it is to try and debug dynamic-typed web stuff. Noooooo thanks.

It's odd how people are in their perspectives on static vs dynamic. I can't for the life of me understand how people tolerate static typing on the web. Every time I've tried it the experience has been so tedious.

I just figure, everything is coming in as a string no matter what. It's going back out as a string. The only place it needs to be anything other than a string is something interesting is being done with it and since dynamic typing lets you just worry about it in those cases, it removes a lot of unnecessary code.

On the flip side, outside of web work i can't imagine the reverse.

> I just figure, everything is coming in as a string no matter what. It's going back out as a string.

Maybe so, but that only accurately describes things at the boundaries of your system. Your database tables still have columns that hold integers, dates, or floats. Sure, you could do the type coercions as needed, but at that point you're missing out on the valuable opportunity to validate your data as it enters the system. You can provide meaningful errors to users as early as possible, you can avoid defensive checks nearly everywhere else within your code, and you can just generally trust that invariants about your data are going to hold throughout the rest of your program.

This leads to smaller test suites, more concise codebases, and higher security assurances overall. If you look at the sorts of vulnerabilities that often pop up in Rails for example, many of them simply don't affect you if you use something like Virtus [0] to validate the types of your data as it enters the system.

I don't do much web development, but statically typed web development is the only way I've ever been able to tolerate it.

[0] https://github.com/solnic/virtus

You can avoid defensive checks anyway. Just don't writre them! /s
You know what else takes in strings and spits out strings? A compiler!

Here are a couple of other under-appreciated facts about compilers:

1. For regular grammars, they are easy to build with any kind of programming language.

2. For regular grammars, they are really really easy to build with statically-typed programming languages.

Now days, my web development is essentially just compiler construction for regular languages, and it just so happens that extremely statically-typed like the MLs (F#, in my case) are damn good for writing compilers (one joke is that it’s the only thing they’re good for). I write far fewer lines of code per feature than I ever did with ”dynamically“ typed languages like PHP, and it’s usually right the first time.

You build your system as a box that takes strings in and sends strings out. You then have to do a bunch of checks (as part of your de/serializing) at the boundaries only. Inside the box, all your logic and transformations are type safe, and indeed you can turn some logic issues into mechanically checked type-level issues, which are basically free unit tests.
I like this for lightweight applications. I imagine it could become a bottleneck for anything moderately heavyweight when it comes to data processing. Most web apps are designed to not do any heavyweight lifting on the client side, so it is a nice general paradigm for that.
Why a bottleneck? Static types don't have to add any runtime overhead. In fact, the type information isn't present at runtime at all in some languages. It's a compile-time guarantee that actually allows you to remove some runtime checks and assertions. And I think all bechmarks show that static languages are as a rule faster than dynamic languages.

If you're comparing two different designs in a statically language where one involves making lots of new types and the other just shuffles strings back and forth, then I can't say and it'd depend on the specific case. Generally, helping the compiler is a good thing, and obviously things like switching on an enumeration is much faster than a long line of string comparisons. Actually, strings are just a horrible datatype, so unconstrained and inefficient. Use strings less!

What typed languages have you used and what aspects do you find tedious? I find that the experience of writing brittle type-checking logic and tests in dynamic languages is rather tedious in its own right.
I find that even the typing experience is faster with static types, since we get better autocompletion/intellisense and there is no need to lookup documentation or other code all the time.
> I just figure, everything is coming in as a string no matter what. It's going back out as a string.

And everything in memory is a sequence of bytes. I don't see the relevance of this line of reasoning. I guarantee that whatever experience you've had with statically typed languages on the web, it's not a good language. Interfacing with dynamic systems is simply a matter of good abstractions, which many languages simply lack.

Take a look at F#'s type providers to see where you can go with this.

Yes everything is a string on the web, but IMO it's not the primitive data types that I'm concerned about so much - it's about the data structure (classes / interfaces). If I refer to my item's description property as `item.descripition`, statically typed language will scream that I have mistyped the property name before letting me run that code or, God forbid, releasing it.
> I have found the whole Elm experience quite mindblowing; if it compiles, then it just seems to work.

My mindblowing experience was facilitated by referential transparency where I had a page of code that became half a page after extracting some functions and restructuring the code and then have that half page collapse into several lines of boring code because I realized that the functionality can be re-expressed in terms of standard functions.

It is amazing to be able to think about an entire branch of code in isolation and be able to understand it in its entirety.

Care to share more about that? It would be really helpful for people looking at the language to see your progression. Could be something as simple as a gist with the code in your different stages.
I tried to look through the commits of that project but could not find something that captures this. Maybe it happened between commits.

Maybe I'm remembering it wrong. Maybe my mind exaggerated the memory. It happened last year while I was implementing Challenge No. 5:

https://github.com/pdamoc/elmChallenges

"if it compiles, then it just seems to work" Man, now that is tempting. Also, how easy are the ports? In ClojureScript you can use JavaScript pretty directly, but I often got a bit hung up translating the syntax (it's not that hard, it was just me).
I've never used ClojureScript so I can't speak for that, but the ports are now a lot easier in 0.17 as an outgoing port is simply a function that you can call, instead of messing about with Signals. Effectively, you send stuff to Javascript by calling the port function with a Javascript compatible value, and then you subscribe to messages on the way back in, which end up triggering a message on the update function of whatever component sent it out in the first place.

TL;DR its pretty easy once you've done it a few times

Ports are essentially event emitters. You can expose them from elm, and in your JS code you attach event handlers to them using .onMessage (I think, been a while).

You can post and receive messages from ports (depending on type). Type checking is done at runtime to make sure nothing breaks.

Cljs interop is easier (can just call a function directly).

Apparently a human-readable homoiconic language is still an unsolved problem...

In 0.17, commands and subscriptions mean that, not matter how deep you nest components, you can still send and receive messages to/from the outside world (i.e. ports).

Thank you for this endorsement. I am learning Elm, I like it a lot so far, but there are times where I question if this is really all necessary.

So first person experiences like yours help me continue and understand where I can be if I continue on the path I am.

Also, I would like to add that I really liked Signals and felt that they are good abstraction, but looking at Subscriptions and they also on surface level make sense.

Did I read correctly that you are using Elm with Cordova?

Perhaps you are using Elm to compile to HTML / CSS / JS which Cordova compiles to native code?

I came across it researching time travelling debuggers to implement in my flux implementation for android.

My mind was blown but never really got enough time to study it since it was low on my priority list.

I think you just pushed it to #2!