I disagree with the post. The goal of traits, or any similar mechanism in other languages, is not to implement a given functionality. It is to describe and enforce a common way and API to expose the functionality.
To take the example given for gleam, there is nothing enforcing that the conversion to string is called to_string, what are its arguments, how formatting works. So you're left to just-so compatibility for each type. They only happen to be the same by accident, and nothing enforces consistency.
Programming language and code in general is about communication between programmers. When I design a trait, or an interface, I'm telling everyone else what are the requirements.
Yeah Traits give you more than just the type of the output, they let you specify how it must be called to get that trait, that's a key capability for writing generic code. Sure I can write a to_string() method but someone else will call it ToString() and I can't use it. If we both impl Display then it's all good.
Same way when you design a type, you define some requirements over raw data.
If one really wanted to argue, that would be that instead of saying that traits/interfaces are types, rather one should say that types are interfaces over raw data.
An interface/trait would then supersede the concept of type.
Constructs like Lisp's CLOS and dd hoc polymorphism can provide the same benefit.
You don't need a class/trait/etc enforcing a contract on data. Instead you can keep data and functionality separate, and then prove you have an implementation of function X for data Y.
This also allows for easy extension of the data downstream.
But Gleam is statically typed, you see, and very restrictively. In such languages, typeclasses/interfaces/extensible records/(void*)-passing and -casting shenanigans are the things that traditionally provide the virtual/dynamic dispatch to the programmers.
This is a bit of a rabbit trail, but something I’ve been wondering about lots of these new languages. Who uses them and why? I’m genuinely asking here. I have a hard time imagining going to a team or customer and saying “c#/go/java/python/javascript are the wrong approach for this project. We need to all learn this new language and use that instead.” Don’t get me wrong: I’m glad people are innovating, and I get that personal curiosity and growth might be good reasons to build a new language. But I just wonder about the real world uses and actual stories behind new and lease known languages. Why did someone create Gleam and how is it being used in production today? What are the long term goals for it? What would be the argument for using it on a company’s projects?
That really depends on which language you're talking about.
An opposite trend I've been noticing is that big languages steal ideas from some smaller language and absorb its community: for example a lot of R ideas have been pulled into Python libraries, resulting in a migration from R to the more capable Python. R's syntax is nicer for statistical computing, but having access to the larger Python ecosystem with slightly worse syntax turns out to be more valuable in the long run.
> Why did someone create Gleam and how is it being used in production today?
I'm an outsider to this community, so I'd trust someone with inside information over myself, but my outside impression is that Gleam was invented to push Erlang's potential for extremely high reliability even farther. Gleam's strong type system lets you create software that rarely fails, and on top of Erlang/OTP's "let it fail" philosophy you have battle-tested patterns for recovering from failures when they do happen. The use case is for networking software which requires high levels of security, scalability, and fault-tolerance, such as cryptocurrencies and end-to-end encrypted messaging systems (it's used by WhatsApp and I think the NEAR cryptocurrency though I can't find a reference for the latter claim).
It's not about creating a "useful" programming language, it's about exploring an idea by implementing it in a working compiler/interpreter and seeing it how well it works in practice. Like other posters have said, the popular "useful" languages will learn from these small experimental languages and incorporate their features once they've proven how useful they are.
> I have a hard time imagining going to a team or customer and saying “
- c#
- go
- java
- python
- javascript
ARE wrong ... for this project.
---
So, what people don't get is that you need somebody crazy enough to make `go, python, java` when C, Cobol and other existed.
You see the major success, but not the attempts in the middle.
For people like us that do languages, not just for fun, we are trying to be the NEXT on that list!. And if somehow one of this languages break it the barrier is because, for certain, they are better than most, at least at certain niche.
---
> But I just wonder about the real world uses and actual stories behind new and lease known languages
A little secret: Creating a new language is the ONLY way to make a paradigm work at scale. You can't do OOP in C, not practically. A new language is the next step after make a framework, and you can see it as the only way to make the idiom of a large scale framework to truly shine without the baggage.
So, in my case (https://tablam.org), I have used +20 programming languages (including SQL dialects that inflate the number!), for real, for work, for pay. None is close to solve the needs of build business apps, and the one that was close (FoxPro) die too long ago. None is relational, none is good for data manipulation, none allow me to encode the business checks, flows or invariants, or make data reports, etc.
"None" here is too strong of a word, right? All that other languages I have used have shipped code! So they "works!", but none allow me to truly embrace the relational paradigm, aka: deal with data.
I think clojure tells a useful story here. Rigid approach, unfamiliar syntax for a lot of developers, and unusual semantics make it a really strange choice. And it never has been popular compared to big languages.
But a lot of companies are institutionally committed to the jvm, and/or have deep expertise within it, and a lot of those have smaller teams or individuals with more freedom to experiment with their tools.
And now clojure has settled into a comfortable niche as the language you use when you should be using java but just don't wanna.
This is one path to making it easier for devs to justify using a new language. And I mean, typescript was in this situation more or less at one point. So sure erlang isn't as popular as java or javascript, but it is a runtime that some places are committed to and have expertise in. I could see gleam latching onto that, maybe.
Well Paul Graham wrote an article on it (http://www.paulgraham.com/avg.html). Essentially, I think the people who pick those languages believe the extra stability/speed/whatever is gained from using it beats the drawbacks.
I can only speak for myself, and I am not involved in the creation of Gleam. But I have to say, an ML style language on top of the Erlang VM (BEAM) is a very enticing combination to me.
I'm also not using it in production at ${dayJob}, so take that for what it's worth. Maybe some day I'll get the opportunity.
I've just built a piece of software and realized it would be a lot better if it could save/restore its state when it restarts. I already have all state isolated in state objects, but they're not plain JSON, they contain instances of classes and things like that. But in virtually all cases I could replace classes with TypeScript interfaces (i.e. just use pure JSON and still get the type-checking benefits).
There are some exceptions where classes need to be instantiated, and I'm thinking, instead of writing a bunch of ser/de, I can just make them static, Replace-All `this` to `self` and add a `self` param. Boom, suddenly save/load is a simple as JSON.stringify() and JSON.parse()
Then I realized I'd reinvented C (plain old data) and Python (explicit self param). Nothing new under the sun!
> Instead of a trait, just make a type that implements the generic behavior you want, and then write a function to convert your data-type into your trait-type
Well, yeah, obviously, when you have a single implementation of a trait/interface, then you can do away with this additional indirection, sure. But what if I have vastly different types implementing the same trait and I want to collect them all in a single list?
> If you need some data-type specific logic, then pass around functions as necessary (usually from your conversion function).
That... basically amounts to manually bundling values of the concrete type together with the implementation of trait and then passing them around together (also manually), isn't it?
I mean, on one hand I get the message: you don't really need any fancy type systems, having integers and bytes and arrays of integers and arrays of bytes for types, ability to declare constants, global and local variables, and functions (not closures) is quite enough already to implement anything. Structs then can be emulated either as short integer arrays of fixed lengths, or with SoA approach: all enums are just integers, and pointers are also but an integer index in some globally known array so yeah, there is nothing much else you need to store in a struct's field.
But let me tell you, programming this way is not very pleasant if you've spent majority of your life writing Python or Haskell. Having common patterns of data and code organization baked straight into the language is nice.
> That... basically amounts to manually bundling values of the concrete type together with the implementation of trait and then passing them around together (also manually), isn't it?
That is more or less similar to implementing the Go interface pointer or Rust "dyn" fat pointer manually. And of-course everyone will do this their own way instead of the language providing the standard.
I make a bet that next major version of Gleam will have traits once its designers are enlightened on the need for the same.
But can you even do that in Gleam? It's statically-typed after all, so what would the types look like? I guess there is gleam/dynamic with its Dynamic type and from() and decodeN() for coercions but...
As usual, simplicity of this type basically means making the programmer write the boilerplate that the compiler could generate for you. The complexity has to live somewhere, and with "simple" languages, it lives inside your own source code, and that of each of your libraries, instead of living inside the compiler.
I'm not sure why this appeals to so many people, but still: it clearly does. So, best of luck to the Gleam team, I hope it lives up to your needs and desires!
This heavily reminds me of old Microsoft's COM's QueryInterface approach, which acted basically the same: interfaces/traits were types. Your code would be like:
basically the same thing as Gleam does, but more verbose and without all that safety.
(I think anyone advocating "going back to basics" would benefit from actually going back to basics and spending some time on those systems. I hope I will never have to touch COM in my life again, but my experience working with it was very helpful. GLib/GObject and Linux VFS are other class-like technologies which are worth experimenting with)
Still, no traits means no runtime dispatch, which is a pretty big convenience feature that neither pattern matching nor a sophisticated type system help with.
This only prints Friend types. How would you loop through a collection of several types and display them? In other languages they could all implement the interface. Perhaps you could have several to_display functions that handle each type but I'm not sure that's valid in Gleam. It would need runtime type checking. In this example to_display is a member function.
Gleam is a strongly typed language like Haskell or Rust, so you cannot have a collection of multiple different types without using another type to wrap them into one.
It looks like the author is confused about or unaware of the difference between static and dynamic dispatch. Their Display and Iterator types are really just vtables; the correct comparison to Rust would be the `dyn Display` and `dyn Iterator` trait objects (which are types).
What makes you say that? Traits and type classes don't inherently use either static or dynamic dispatch (Rust and Haskell use both, PureScript uses only dynamic), and both techniques outlined in the blog can be optimised to static dispatch.
In some ways, in Haskell, type-classes are passing around "dictionaries". You can implement a similar pattern as in Gleam, in Haskell, by explicitly passing around those dictionaries. In Haskell it makes for some pretty ugly code though.
But type-classes get a dual use also as constraints for parametric polymorphism. This gives you some more immediate feedback from the type checker and also lets the programmer extend type checking with their own rules.
Never the less I sometimes use this pattern of grouping functions together into a record along with pointers and handles they need to shared resources to make running those functions, passing them around, and cleaning them up easy and explicit -- when it is called for.
From experience I find I doubt that we tend towards complexity or enjoy writing complex code. Most people I know find complex code takes quite a lot of effort to write, understand, test, and make useful. The majority of people I've worked with want to write simple, elegant code and find joy in understanding a problem. It happens that some times we either judge the problem to be more simple than it is or we can't formulate a succinct specification of the solution. This leads to code needing some solution to the inherent complexity in the former case. And in the latter it means we don't know what we're doing and are getting lost in the weeds. In both cases a liberal application of good-old-fashioned thinking tends to do the trick.
When thinking is insufficient it always helps to have coding guidelines, mentors, and access to experienced colleagues. The last ditch effort of course is searching the good, old Internet.
I've realised a long time ago that all of programming essentially consists of two things: data, and procedures that operate on that data. Everything else is in function in one or both of these two things.
For example - functions and methods organise the procedures in smaller, more manageable (from human perspective) chunks, types formalise and limit the data to make them easier to transfer and organise, and classes (are supposed to) do both.
Well a function is like an interface (or trait as newer languages like call them for some reason) with one member. The problem the article seems to totally skip over inexplicably is that the point of an interface (trait) is when you have more than one implementation.
Rust's Display is superior to everyone else's To string precisely because it is stream-oriented rather than allocation-oriented. Allocating f-strings are a major language smell.
Like many good posts, this one got me thinking, and elicited a visceral reaction.
The idea of traits as types is potentially interesting from a pedagogical perspective, since it gives us another way of thinking about things. But whether it is simply analogous or is in fact homologous is the key question: If simply analogous, then we need to understand the limit of the analogy, where it works, where it fails. If homologous, then we have a powerful tool to draw upon, much like how mathematicians can switch analytic techniques equivalent over whatever category they are working with, or how physicists change coordinate systems to make the math and understanding easier for particular types of motion.
I tend to think it is simply an analogy, but I don't yet know where its limits lie.
> Really the only difference is that we manually convert from Friend to Display in Gleam, where Rust already knows how to use Friend as a Display.... Instead of a trait, just make a type that implements the generic behavior you want, and then write a function to convert your data-type into your trait-type.
Aaaand here's where my gut twists. Why? Why would you do that? Why would you ever do "instead of... -> just" where the compiler will do it for you?
Twice in that quote the author espouses writing more code because reasons, and, honestly, I don't think the reason is any better than "Gleam doesn't haven't traits and we're all OK with doing the extra work".
That's fine. If that's how someone wants to work, and if their code works for them and is as maintainable as they need it to be, great! I cannot gainsay that.
And I'll note that for someone who hasn't yet grokked generics and traits, the Gleam code may be more readable, because things are more spelled out.
Sort of reminds me of my early Javascript, before I got comfortable with promises, composition, lambdas, and a few other things: Very readable blocks of code, often many lines long, that I would now replace with a single line.
But once one understands traits and constraints, one can write very powerful, and, thanks to how Rust works, safe code that other (oh, I do have to use that term? oh, ok) Rustaceans will grok quickly, code that will likely also be performant.
If I can save myself time and screen space AND make it clear to others what's happening AND make it safe and performant, I'm in.
(I'll just remember to add comments as to why this is a single line and why that isn't.)
Traits are behaviors. But they are not called behaviors as that would be too clear. For example, we know that traits are personal "uniquely identifying characteristics" and that behaviors are general cross-cutting categories, but for this "mixin" we opt to use the personal 'trait'. We also know we're not just describing something but also have "implementation", but hey let's not call it behavior and give the game away!
One of the biggest headaches in software (historically) is how things have been named. ("Dynamic programming" says hello).
"A trait contains a set of methods that implement the behaviour that it provides. In general, a trait may require a set of methods that parameterize the provided behaviour.
"Traits cannot specify any state, and never access state directly. Trait methods can access state indirectly, using required methods that are ultimately satisfied by accessors (getter and setter methods).
"The purpose of traits is to decompose classes into reusable building blocks by providing first-class representations for the different aspects of the behaviour of a class."
The trait itself is not the concrete implementation, since a trait can have many different concrete implementations.
A trait is, for all, or at least most, intents and purposes, an interface that can have an optional default implementation of some of the functions inside it.
Traits, interfaces and types can also, for all, or at least most, intents and purposes, play the same role. Define a contract of stuff implemented by a code entity.
Of course any added special semantics depend on the language.
To take the example given for gleam, there is nothing enforcing that the conversion to string is called to_string, what are its arguments, how formatting works. So you're left to just-so compatibility for each type. They only happen to be the same by accident, and nothing enforces consistency.
Programming language and code in general is about communication between programmers. When I design a trait, or an interface, I'm telling everyone else what are the requirements.