top | item 37158068

Dart 3.1 and a retrospective on functional style programming in Dart

146 points| mdhb | 2 years ago |medium.com | reply

194 comments

order
[+] grumblingdev|2 years ago|reply
> It’s typical to listen to this stream of events and use chained if-else statements to determine an action based on the type of the events that occur.

You'd think something like directory watching would have a clear set of events that would make nice objects with consistent meanings, but in my experience file watching gets crazy complicated, and can have all sorts of edge cases.

Just take a looked here for all the various edge cases that crop up: https://github.com/paulmillr/chokidar/issues

Then you have linux, windows, macos, and maybe you want to abstract over some underlying implementation like chokidar vs fb/watchman vs webpack/watchpack. Every new OS release could also cause things to change. A big leaky abstraction.

So usually its going to be a bunch of if-else statements hacked together to get around edge cases, and have to be revisited later on.

Any attempt to abstract this into objects, just obfuscates things. And OO forces you to name things, when in fact they might be un-nameable. `FileSystemModifyEventExceptWhenXAndYAndSometimesZ`.

The behavior might rely on a series of events together, so the object hierarchy must be re-worked.

OO has this rosy idea that we just have to come up with the perfect hierarchy, but things change in unexpected ways, and everything must have a descriptive noun.

[+] munificent|2 years ago|reply
For what it's worth, I've written plenty of code using file watching. I did a build system that needed to watch the file system to pick up file changes.

You're absolutely right that file watching gets really hairy really fast. Sometimes you won't get events for files. Renaming directories can make things get all kinds of weird. Sometimes you'll get a flurry of updates for the same file and you need to debounce. It's a mess.

But the actual atomic primitive events themselves I've found to be fairly simple. It's basically just write, delete, and rename. I think modeling that as a sum type of a sealed class hierarchy works fine.

Most of the problems are a level above where the set of events is hard to parse meaningfully.

[+] taeric|2 years ago|reply
I've long thought it was less OO that was problematic, and more the idea that you can make a taxonomy that covered your entire problem domain.

Worse, people seem to get caught up in the taxonomy for the sake of the taxonomy.

[+] the_gipsy|2 years ago|reply
The hierarchy always looks nice on the whiteboard but always breaks down as soon as you start to write it out in code.
[+] coldtea|2 years ago|reply
Directory watching would be more of a "finite state machine" kind of handling than "reacting to a set of possible fs events" handling.
[+] zengid|2 years ago|reply
From the article:

Functional languages generally implement algebraic data types by pattern matching over the cases in a sum type to assign behavior to each variant. Dart 3 achieves the same with pattern matching in switch cases, and takes advantage of the fact that OO subtyping already naturally models sum types.

I like that they're saying this part out loud, while languages like C# have been able to do this for years but they never came out and spoke about it in this way. It might have made folks get excited about C# when everyone was really excited about how nice Rust's enums are (they're still exciting to me though).

[+] andreypopp|2 years ago|reply
Does it (Dart, C#) support exhaustivity checking for such matching over subclasses?
[+] trashburger|2 years ago|reply
I'm all for ADTs but the reasons the article gives are all wrong, and in fact I think it makes the code a lot worse as a result.

Object programming doesn't mean "inheritance trees", that's just the (IMHO overly simplified) flavor of object programming that we call "OOP". (I'm omitting a full rant from here, which I ought to put on an article at some point.) Object programming is all about state and behavior, just like functional programming; it only differs in how it lays out the behavior namespace (object programming uses inheritance of behavior through lookup chains, which puts the object in focus; functional programming inverts that and uses the structure of the object to dispatch, putting the function in focus).

Grouping behavior together in a single function like the article suggests is, IMO, an anti-pattern and one that I consider to be one of the great disadvantages of a functional programming style. Bundling behavior together means the behavior is out of the control of the object; the object's identity now becomes a critical part of the operation being performed. IMO, putting the focus on behavior is the wrong approach here.

Don't get me wrong, I'm all for some features in functional programming; immutability is great for being able to make assumptions about program state and re-entrancy, for example. But I think the tendency to "lock" behavior into functions and not letting objects define their own behavior is a major shortcoming.

[+] sirwhinesalot|2 years ago|reply
Making everything an object is a cute idea but the fact of the matter is, most of the time data is just data, there's no associated behavior.

Lets say you have a Player object who has an Inventory (can also be an object) and an inventory contains items like Swords and Bows etc. When a player wielding a sword takes a swing at an enemy, a particular animation has to play, and sword swing and sword hit sound effect has to play.

Which object contains the code that performs the rendering of the animation? Is there an "Animation" object that contains it? Does it call some setPosition/setRotation object in the sword and the player? Does the Animation object know how to call the appropriate Metal/Direct3D/Vulkan APIs or is that another object?

Or is the right solution to have an animation subsystem that doesn't care at all about "players" or "swords" and is only concerned with mesh data and how to transform it? Answer: the latter, which is what every game engine that wants to be even remotely efficient does.

That's not to say the Smalltalk idea of building larger systems out of smaller systems communicating via message passing is bad or anything, the problem is the OO idea of tying data to behavior. There isn't a single OO codebase out there without POD objects.

[+] isaiahg|2 years ago|reply
I feel like this is mostly a straw man argument or maybe it comes from a lack of experience in functional programming. Functional programming to me is more of a process to problem solving and less organizational. (Though it can be a part of it) Indeed you can combine OOP and functional styles very easily. Scala is a great language that did just that to great success.
[+] _old_dude_|2 years ago|reply
It depends if the hierarchy is open or closed.

If the hierarchy is open, the behavior is defined in each subtypes and it's easy to add a new subtypes but you can not add a new operation (a new method). That's the OOP way.

If the hierarchy is closed, the behavior is defined in each function, it's easy to add a new operation (new function) but you can not add a new subtype. That's the ADT way.

This is known as the https://en.wikipedia.org/wiki/Expression_problem

[+] mrkeen|2 years ago|reply
I think you have it backward:

> But I think the tendency to "lock" behavior into functions and not letting objects define their own behavior is a major shortcoming.

"Locking behaviour" happens when you bundle objects & functions together.

For instance, an OOP programmer might create a binary tree, mark the fields as private, and expose a public visit() method.

If you take away the "objects defining their own behaviour", then you're left with just the binary tree. Then different callers can write their own visit() functions.

[+] zogrodea|2 years ago|reply
If you want to override behaviour, why not just define a sum type wrapping other objects, a master function that matches on different cases, and then smaller functions that decide on what to do for each case? That seems fine to me.

Like this:

type obj1 = { ... }

type sum = | One of obj1 | Two of obj2 ...

match param with | One obj -> f(obj) | Two obj -> f2(obj)

----

There is also the handle pattern which I've used with some success. You basically place some functions in a record/tuple and then call those functions when you need to.

https://jaspervdj.be/posts/2018-03-08-handle-pattern.html

Edit: I mean to say that I've used something similar to the handle pattern in an immutable context (having a central function that takes objects that can be arbitrarily nested which carry their own function and data, and the parent object can call its function on a child object, and the child can call its function on its child, and so on...). Hopefully I am making sense.

[+] grumblingdev|2 years ago|reply
> Bundling behavior together means the behavior is out of the control of the object; the object's identity now becomes a critical part of the operation being performed. IMO, putting the focus on behavior is the wrong approach here.

Why? Do you have an example to better explain?

If find people rarely talk about the evaluation criteria for which approach to programming is better. That is, what are you optimizing for?

[+] cageface|2 years ago|reply
In non trivial code you often need to make coherent atomic updates across a whole section of your state graph. This is very difficult when the code to update state is fragmented across many classes. For me this is where OO really falls down and more functional approaches shine.
[+] randyrand|2 years ago|reply
Dart is a seriously good language.

Shares a lot in common with C#, and is just incredibly productive.

[+] onli|2 years ago|reply
I also loved it, coming from mainly Ruby. It's just reasonable, the step here toward functional programming gets it even closer to the way I like to write Ruby.
[+] no_wizard|2 years ago|reply
The one fork of history I wish I could have seen come to fruition was Dart in the browser (natively) as was originally proposed. I'm very curious to see how web development would have evolved if that had come to pass.
[+] jmull|2 years ago|reply
It's hard to see how that would have worked.

Google can put anything it wants in Chrome, but why would Safari or Firefox have done this? I don't see how they even could from a practical perspective, since they'd each either have to maintain a separate implementation (you really find out how good your specification is only once it needs to support multiple independent implementations) or import the Google one wholesale. Rinse and repeat every time Google changes Dart.

If Google puts it in Chrome and pushes it, most likely it simply dies there. But perhaps it catches on, furthering the reality that Chrome (and therefore Google) is the web. That's the worst case scenario for everyone (except Google, of course).

Anyway, WASM is here, and doesn't have these drawbacks, so we're all free to enjoy using Dart in the browser, if we want to (to the extent they support it). Not to mention Javascript is in quite a different place than it was when the idea of native Dart in the browser came up.

[+] munificent|2 years ago|reply
I work on Dart and I'm honestly glad that didn't happen. It would have frozen the language in time and features like the better type system in Dart 2.0 and sound null safety in 2.12 would have been unlikely to happen.
[+] pron|2 years ago|reply
Brian Goetz published a similar article about Java a year ago: https://www.infoq.com/articles/data-oriented-programming-jav... (taking care not to call it "functional style programming" but rather "data oriented programming")
[+] dgb23|2 years ago|reply
Thank you for sharing this one!

Also there are interesting talks published on YT where he discusses data/functional programming, OOP etc.

Multiparadigm (including plain old procedural) seems to be the most pragmatic approach to me, because I don’t need to bend over backwards to express something in a particular way.

Aside: It’s really fascinating how Java and the JVM manage to evolve while retaining reliability.

He talks about that here: https://youtu.be/2y5Pv4yN0b0

[+] frou_dh|2 years ago|reply
Poor Martin Odersky was demonstrating this fusion with Scala like 15+ years ago. But he's a nice guy so I'm sure he thinks the more the merrier!
[+] jononomo|2 years ago|reply
Shouldn't Dart be merged with Flutter and be thought of as a single animal? There doesn't seem to be any point in thinking up scenarios for Dart that don't relate to Flutter.
[+] Daegalus|2 years ago|reply
I loved Dart, but these days, it's not very usable for me.

The backend development capabilities have been left to rot. It's all flutter.

When it came out it had so much potential to displace NodeJS. But they threw that away when they couldn't get into the browser and pivoted to flutter.

I still maintain some of my packages like uuid, otp, etc. But my API handlers and other backend stuff is kinda dead as there is 0 interest in it nowadays.

I would use Deno before Dart for backend nowadays, even though I would prefer Dart.

[+] toastal|2 years ago|reply
Is there a functional language that compiles to Dart? There’s a difference between FP features & FP ergonomics. This seems like a very verbose way to go about things I’m used to being simple/easy.
[+] ollysb|2 years ago|reply
I've been working with Elixir and Elm the last few years but recently have been working on a mobile app in Flutter. While the recent changes are a good step in the right direction (being able to express ADTs and records) it does feel like an iteration away from being ergonomic.

Regarding sealed types, I'm not really sure why you'd choose these over type unions, in practice they're often used to describe things like events for reducers and using sealed is incredibly verbose compared to the equivalent in for instance typescript.

    type Message = 
        | { type: 'IncrementBy', value: number }
        | { type: 'DecrementBy', value: number }
        | { type: 'Set', value: number };
vs

    sealed class Message {}

    class IncrementBy extends Message {
      final int value;
      IncrementBy(this.value);
    }

    class DecrementBy extends Message {
      final int value;
      DecrementBy(this.value);
    }

    class Set extends Message {
      final int value;
      Set(this.value);
    }

In a real app this noise really adds up making it difficult to get an overview of the types.

Record types are nearly there, but currently there isn't a way to update the records which really hobbles them in day to day use (it looks there will be record spreading coming though so this is hopefully temporary).

[+] tym0|2 years ago|reply
There's a Clojure for Dart I believe.
[+] bb86754|2 years ago|reply
F# has something called Fable. It originally just compiled down to JavaScript. Recently they've added Python and Dart.
[+] activitypea|2 years ago|reply
> Object-oriented (OO) and functional languages differ in many ways, but it’s arguable that how each paradigm models data is the defining characteristic that separates them. Specifically, the question of modeling different variations of related data and the operations on those variants.

Then why is the first example they present about organizing behavior lol

[+] leetrout|2 years ago|reply
Since learning Go so many years ago it influenced how I build things in every language much in the way this article is espousing.

I MUCH prefer functions that take some kind of type/interface/protocol over methods on objects.

I committed many sins in my past in Python with mixins that added functionality before I understood how to use Zope Interface and with Go it all finally clicked for me.

I don't think any pattern is Right all the time but inversion of control and interfaces rarely bite me to the same degree other patterns have.

[+] ac130kz|2 years ago|reply
So sad there is still no static metaprogramming. I've switched my career from a Flutter mobile dev over to backend, and there are still no updates on this an utterly important feature for Dart. This could've eliminated so much sickening codegen.
[+] ducktective|2 years ago|reply
I'm seeing Flutter is gaining more momentum at least in social media. Last I checked, I concluded the framework inherently depends on layers of abstractions that make it slower/less-responsive than say QT or native toolkits of the OS.

Since Dart's raison d'etre is pretty much Flutter dev, I was wondering if it is worth learning if one's intention is knowing a handy cross-platform GUI dev toolkit that just works.

That said, the competition in this scene don't have better to offer anyway namely react native (web tech) and QT (LGPL).

[+] tasubotadas|2 years ago|reply
The last time I've checked, OOP and functional programming are orthogonal concepts.

EDIT: It means that there is no problem for the language to have both OOP & Functional. They don't have to choose one over another.

[+] chriswarbo|2 years ago|reply
> there is no problem for the language to have both OOP & Functional. They don't have to choose one over another.

Sure a language can have both; but us programmers still have to choose one or the other when modelling some domain. The article is basically describing the "expression problem": how to implement a solution in a way that allows new cases to be added https://en.wikipedia.org/wiki/Expression_problem

The OOP approach makes it easy to add new sorts of data: we just write a new subclass, with implementations for all the interface's methods. However, it's hard to add new sorts of operations: we would have to add new methods to the interface, and update every subclass to implement those methods (very hard if we're writing a library that third parties are subclassing!)

The functional/ADT approach makes it easy to add new sorts of operations: we just write a new function, which pattern-matches on all the constructors. However, it's hard to add new sorts of data: we would have to add new constructors to the ADT, and update every function to pattern-match on those constructors (very hard if we're writing a library that third parties are pattern-matching on!)

When we're choosing how to represent something, it's important to consider whether future extensions are more likely to be adding new sorts of data (in which case the OOP representation may be best) or more likely to be adding new sorts of operations (in which case the ADT representation may be best). Having to extend such representations in their intended way can feel very natural and satisfying; but having to extend them the other way can be frustrating and laborious.

[+] kaba0|2 years ago|reply
Why would they be orthogonal? There are entire languages built on top both, like F# and Scala.
[+] Cthulhu_|2 years ago|reply
That must've been a while ago, I find factors of both working pretty well together. Most languages are a mix nowadays anyway.
[+] lbhdc|2 years ago|reply
I have really liked dart, and started playing with it when it came out. I just wish I could use it more. Unfortunately it and its tools are so opinionated its kind of hard to use outside of the specific cases they have dreamed of. I really wish there were opt outs, even if it broke things that would be worth it.
[+] Alifatisk|2 years ago|reply
Any updates on type union? Using dynamic removes all the type hints.

I’d like to statically type which two types a function might return.

[+] kalekold|2 years ago|reply
Rob Pike was right: "features were being added by taking them from one language and adding it to another" "...and I realised all of these languages are turning into the same language." "we don't want one tool, we want a set of tools, each best at one task"

https://www.youtube.com/watch?v=rFejpH_tAHM

[+] threatofrain|2 years ago|reply
Dart's reason for existing is as a cross-platform GUI language via Flutter. It's not obvious that adding on some language ergonomics detracts or adds to that goal in a fundamental way.

Sometimes languages converge because of informal consensus that something is a good idea. Maybe Go adding generics is a good idea, even if it's a step in the direction of following trends.

Either way, most languages don't have syntax which is significantly challenging or easy (with exceptions like Rust), and it's the ecosystem which makes the language so productive. So whether or not Go added generics today or a decade ago, Go was still great anyway thanks to its standard library and ecosystem.

[+] Cthulhu_|2 years ago|reply
This is one reason why I love Go, in that it doesn't follow.

I've been primarily doing Javascript and the like for the past decade, and while I appreciate new features like functional operations added to the standard library - although that was a long process - and arrow functions, I lost it when they added object-oriented structuring like classes, but only half an implementation because there wasn't any good access modifiers. And they added OOP before they added modules and imports. I don't understand.

[+] kaba0|2 years ago|reply
Deliberately being different from other languages… that actually makes a lot of sense why go is the way it is.

And I don’t mean it in a good sense.

[+] DrScientist|2 years ago|reply
I'd agree - and I'm not sure it's even possible to have a language with every feature.

Languages are about telling computers[1] what to do - at the right level of abstraction for the task in hand.

As far as I can see ( as an outsider ) is the trick in language design, is not the programmer ergonomics per se - but creating a set of abstractions ( expressed as language features ) that can then be converted into machine code in a way that's some best combination of reliable, safe and fast.

[1] Not directly, but indirectly via compilers or interpreters.

[+] pjmlp|2 years ago|reply
Thankfully not everyone happens to agree with him.

Had it not been for the success of Docker and Kubernetes adoption, and Go would have been as successful in the market as Plan 9, Inferno and Limbo.

[+] pharmakom|2 years ago|reply
this view does not allow for situations where one language design is strictly better than another, which has happened a bunch of times in practice.