I like OCaml and have written the "why we chose XYZ language" posts. Most of the time the real answer is "we like it and it makes us feel good to use it". Like the answers aren't wrong per se but they're more post-facto justifications. And that's perfectly fine! I think we should normalize saying that tech stack choices are subjective and preference-based. We're not robots. The social and aesthetic parts of a stack matter to people
Definitely agree. Most teams don't choose a language for purely rational reasons, and we're not pretending we did.
We like OCaml, it makes us excited to build. We know the language deeply, which means we can reason about performance and behavior before we run the code. We can onboard new engineers quickly because the type system forces clarity.
The runtime is simple enough that we can predict what it's doing. So yes, part of it is that OCaml feels good to use. But that feeling comes from years of watching it make complex systems simpler to reason about, not harder.
One of the great understated reasons for using 'cool' programming languages is that it allows a business to hire selectively for people for whom programming is a passion.
I think that in some cases where OCaml was chosen, like Docker or some parsers, the choice was obviously not evangelical. But I agree that in most cases it is a post-facto justification.
Exactly! OCaml is the language I like to solve problems in, and I'm excited to solve problems in, so that's why Terrateam uses OCaml (I'm the CTO). You can do a lot (but not all) of this in Go, or TypeScript, but I don't get excited about those languages. Certainly I'll use them if I have to (our UI is written in Svelte) but building your own company is a grind, and using OCaml makes the grind just a bit more exciting, and that's an edge.
> I think we should normalize saying that tech stack choices are subjective and preference-based. We're not robots. The social and aesthetic parts of a stack matter to people
I would just like to distinguish "subjective and preference-based" from "social and aesthetic" and also clarify some notions.
1. The social is objective. We are social animals. It is essential to what it means to be human. We need social relations to grow and develop and to become more human.
2. The aesthetic is objective. We confuse taste with beauty, and this is perhaps the legacy of influence of certain philosophical traditions on our thinking. Beauty has to do with the fullness with which some thing instantiates a form and realizes some end/good. So, when it comes to artifacts like programming languages, a beautiful language will satisfy some human purpose more perfectly than a less beautiful language. Taste is a matter of subjective disposition to beauty. Someone with bad or poor taste might prefer the inferior over the superior, for example, or fail to discern between the two.
We sort of create mystery about preference here, as if they were just arbitrary, immutable, inexplicable brute facts. But preferences can be more good or less good or even bad. Note the relation between preference and taste.
> We're not robots.
3. Typically - and I do not accuse you of this - this is meant to mean that what makes us human compared to robots is that we have emotions. But it isn't that. Many animals have emotions. What makes us distinct as human beings is the intellectual and the rational, which robots (as computational instruments) are not.
4. Post hoc rationalizations may not stand behind the actual motivations, but the content of the rationalization may remain true and valid nonetheless.
I’m sorry but there is no way you can demonstrate a universal aesthetic. Your opinion of other people’s tastes does not reflect on their taste — it reflects on yours.
I have worked with Haskell, Scala, and OCaml; they all bring the joy of programming into daily tasks, and OCaml has a fast compiler and a great module system. This makes it a really fun and effective language to use.
The Ocaml module system is great, but the module system in Scala isn't the usual Java package thing... it's traits. It's about as powerful as the OCaml module system on any axis I've ever used, but it's easy to miss how powerful it is. (Scala 3 added some ergonomics to make it easier to use, but it was all technically accessible in Scala 2 with 'workarounds'.)
Scala just seems to have an ever changing identity. Scala 3 drastically changed syntax and now they're trying to move the language from monads to effects.
This is pretty much obvious for people migrated from JavaScript to TypeScript and suddenly realised that most of their unit tests can now go to a trash bin.
I’m speaking from a Python background. I love types and use them religiously, but I had no idea how much better (modulo runtime checks; that one’s obvious) others were at it.
It's been 8 years and I can't imagine ever writing untyped JavaScript again. Garbage garbage garbage.
If you're on TS and want end-to-end type safety, I recommend you validate everything with something like Superschema and Zapatos/PgTyped. Node with 100% type safety is wonderful to work with.
Here's my take. It helps you enforce properties about your data. Didn't mean to make this response so long, but alas.
(*
Quick note on notation: I will use "double quotes" when referring to _values_ and `backticks` when referring to _types_.
*)
(*
Think of this as an interface. It defines the shape of a module. Notice that the interface describes a module that defines a type called `t`, and two values: "of_string", and "to_string", and they are functions with types: `string -> t`, and `t -> string`.
*)
module type ID = sig
type t
val of_string : string -> t
val to_string : t -> string
end
(*
Below this comment is a module named "Id" that _is of type_ (in other words: it _implements the interface called_) `ID`. Due to the explicit type annotation (Id : ID), now from the perspective of anywhere else in the code, the exported interface of the module "Id" is `ID`.
Modules only contain two things: `type declarations`, and "values". Values are your primitives such as 1, '<', "hello", but also composite such as (fun x -> x + 1), (Some x), f x, { foo = "bar"; baz = 42 }, and even (module Id) (yes! modules can be values too!). Type declarations tell the compiler . Anything which is a value _always_ has a type that can _usually_ be inferred.
No type annotation is necessary when the compiler correctly deduces the type of your value through static analysis. For instance, in the module below, "of_string" is deduced to be of type ('a -> 'a). The ' on the symbol 'a signifies a "type variable", and it means that it can be filled in with any type. For instance (t -> t) and (string -> string), but not (t -> string) or (string -> t). For those it would have to be of type ('a -> 'b). We cannot deduce this type, however, because our implementations do nothing with their inputs besides return them. Since nothing is changed, it's always the same type.
Now, can you spot the pink elephant? Notice how the "ID" interface from above defines "of_string" to be of type (string -> t). How can this be possible? It's because we gave the compiler a hint when we said `type t = string`. This says that a "value" of type `t` is backed by a value of type `string`. If something type checks as `t`, it also type checks as `string`.
So, we could reason through and say ('a -> 'a) can be instantiated to (t -> t), but `t` is also equal to `string`, so we can mentally imagine a hypothetical intermediate type... something like ({t,string} -> {t,string}). This type and type equality is visible _inside_ the module. But when the `ID` interface was applied over the `Id` module as in (Id : ID), this has the effect of hiding the type equality (the fact that `type t = string`) because in the `ID` interface we define `t` without an equals sign: `type t`. This forces us to _choose_ a concrete type to expose externally, even though the type is less general than what the implementation sees.
NOTE: OCaml doesn't use parens for function definition or application. Compare this OCaml code against its Python equivalent.
> let hello_world h w = (h, w)
> let h, w = hello_world 1 2
vs.
> def hello_world(h, w):
> return (h, w)
> h, w = hello_world(1, 2)
*)
module Id : ID = struct
type t = string
let of_string s = s
let to_string s = s
end
let main () =
let s = "abc123" in
let id = Id.of_string s in
(* NOTE(type error): because the built-in "print_endline" function is of type (string -> unit) and not (Id.t -> unit) *)
(* NOTE: if an expression returns unit, you don't need to create a let binding for it. You can simply tack a semicolon to the end of it if you need sequence another expression to follow it. *)
print_endline id;
(* okay *)
(* STDOUT: abc123 *)
print_endline (Id.to_string id)
;;
main ()
You could imagine implementing this pattern of defining parsers such as "of_string", "of_bytes", "of_json", "of_int", "of_db_row", "of_request", for any piece of input data. You can think of all of these functions as static constructors in OOP... you take in some data, and produce some output value: e.g. "of_string" takes in a `string` and produces a `t`.
Now, if you have a bunch of "values" of type `t`, you know that they _only_ could have been produced by the `of_string` function, because `of_string` might be the _only_ function that ends with `-> t`. Therefore, all the values maintain the same properties enforced by the `of_string` function (similar to class constructors in OOP). With this, you can create types such as `Nonnegative.t`, `Percent.t`, `Currency.t`, `Image.t`, `ProfilePicture.t`, and parsers from another type to the newly minted type.
The compiler can help you enforce these properties by providing guardrails in the form of static compiler checks (these checks are run _before_ your code can even be compiled). If I have a value of type `Nonnegative.t`, then not only do I not need to validate that it's not negative, I also don't have to validate that it's not negative everywhere else that values of that type are used -- the validation logic is baked into the constructor. Parse, don't validate.*
> Most systems handle this defensively with locks and runtime validation.
So i work at an org with 1000s of terraform repos, we use the enterprise version which locks workspaces during runs etc.
everywhere else i’ve worked, we either just use some lock mechanism or only do applies from a specific branch and CI enforces they run one at a time.
My question is: who is this aimed at and what problem is it actually solving? Running terraform isn’t difficult - thousands of orgs handle it no problem - the issues I have with it with it have never been around lock contention and race conditions..
Hello, CTO of Terrateam here, the creators of Stategraph.
As you said, the common practice is to use locks on state to guarantee that operations don't step on each other. This works, however the cost is that if it takes 5 minutes to perform an operation, only one person can be doing an operation at a time, so if 5 devs are modifying infrastructure, the last one has to wait 25 minutes just to get back the plan, even if those 5 people are not changing overlapping resources in the state.
The way that most people deal with this is they take their infrastructure and break it up across multiple root modules, and then when those root modules, break it up again, etc.
Stategraph is solving the problem of getting all of the performance benefits of breaking up your root modules without breaking up your root modules. It dynamically determines which resources each of those 5 devs are operation on and, if the resources do not overlap, can run them in parallel.
That means Stategraph is manipulating state in a bit more sophisticated way than standard Terraform/Tofu, and we need to be careful we don't get it wrong.
I'm not sure I would want this even if I could have it TBH. Engingeering org size is about ~200 with infra/sre/ops around ~25.
Different teams want to move at difference cadences. At a certain scale splitting up things feels a little more natural (maybe I am stockholmed by prior limitations with TF though or just used to this way of operating now).
But even then, we're moving to k8s operators to orchestrate a bunch of things and moving off terraform apart from the stuff that doesn't change much (which will eventually get retired as well). Something like https://www.youtube.com/watch?v=q_-wnp9wRX0
Terraform variable management is our larger problem (now/nearterm) when we have to deploy numerous cells of infra that use the same project/TF files with different variables. Given the number of projects/layers of TF getting cell specific variables injected is meh.
Those variables are instance size, volume size, addresses, IAM policy, keys etc.
This is in the b2b saas world with over a million MAU. We've got islands of infra for data soverignty, some global cells where each cell can communicate back / host some shared services (internal data analytics, orchestration tooling, internal management tooling and the like).
Back in the day, before git, we had RCS. Developers would just lock files when they worked on them, and then unlock when they were done. Or you'd copy a folder manually ("branch") to work on things concurrently and then punch them in the shoulder when they forgot to unlock master so that you could lock it and check in. It worked absolutely fine, there were loads of workarounds!
> The concurrent applies isn’t that big of a deal?
That depends. There are many organizations (we talk to them) which have plans and applies that take 5 - 10s of minutes, some even close to an hour. That's a problem. We talked to one customer that a dev can make a change in the morning and depending on the week might have to wait until the next day to get their plan, and then another day to apply it, assuming there are no issues.
If you're in that position you have two options:
1. Just accept it and wait.
2. Refactor your root module to independent root modules.
(2) is what a lot of people do, but it's not cheap, that's a whole project. It's also a workflow change.
Stategraph is trying to offer a third option: if your changes don't overlap, each dev can run independently with no contention.
Even if one doesn't think contention over state is a big deal, I hope that one can agree that a solution that just removes that contention at very little cost is worth considering.
Sorry for the large aside, but anyone knows the whereabouts of the Flambda2 project? Can't find the GH repo anymore, only this fork I didn't know about: https://github.com/oxcaml/oxcaml/
That's the repo, Jane Street has rebranded their OCaml fork to OxCaml (as in oxidised, Rust-like). From the readme:
> This is also the home of the Flambda 2 optimiser
Their plan is to use OxCaml as their experimental fork and work with upstream to port features from it. Labelled tuples and immutable arrays for example landed in OCaml 5.4 but were originally from OxCaml.
We're still figuring out what balance makes sense between openness and sustainability, and we'd rather take the time to get it right than rush into a license we'll regret later. The goal is for Stategraph to last a long time.
>One operation can't corrupt another operation's view of state because state is immutable by default.
How true is this in practice? I mean on the one hand sure Operation 2 doesn't seem some half modified state from Operation 1. On the other hand Operation 2 now has some stale state and makes the wrong decisions does the wrong thing because it didn't see Operation 1's changes.
That happens whether immutable or not. In the mutable world, you have to guard that using a mutex or something. In that case, operation 1 may be blocked by operation 2, and now you get a "stale" state from operation 2. But that's okay. You'll get a new state next time. The real problem occurs when two states are mixed and corrupted.
I don't really get what's special about OCaml with these points they raise? Wouldn't almost any strongly typed language do? Wouldn't TypeScript also tick all these boxes?
EDIT: I wouldn't choose TypeScript either for this type of use case, but not for the reasons they state, that's my point
Typescript (javascript) is kind of a joke compared to what ocaml brings to the table.
Ocaml has a top in class typesystem, a "faster than Go" compiler and (in 2025) good tooling. It allows you to say fuck it and write a while loop if you need to. Hell you can even do OOP. Also it has an incredible module system and full type inference. It also has an effect system, and good concurrency features (ocaml 5).
I cant say many other languages that has all the same features.
It's the combination with concurrency that makes this a hard problem.
And OCaml excels at solving that sort of problem. OCaml and Erlang are the only two languages that I'm aware of that have a really clean way of doing this, in most other languages there is always some kind of kludge or hack to make it work and at best you're going to do something probabilistic: it seems to work, even under load, so it probably is good now. Until six weeks later on an idle Tuesday the system deadlocks and you have no idea how it happened.
What advantage does OCaml have over Haskell here? I find software transactional memory in Haskell so simple to work with that I have lost all fear of concurrency, but what am I missing out on?
TypeScript does not have an equivalent to PPX infrastructure AFAIK. If there is, it's definitely not as widely used within the ecosystem compared to PPX for OCaml.
Besides the better type system, OCaml is a compiled language, you don't need workarounds like rewriting code in Rust and Go, as it happens on TypeScript/JavaScript world.
Certainly there are specifics between the type systems that differentiate. TypeScript generally chooses to enforce types in an ergonomic way whereas OCaml chooses to enforce types in a sound way. Whether or not that is a meaningful differentiator for someone is up to them.
This blog post shows the elements of OCaml that motivate us to use it. Is it complete? No. Maybe it should be more explicit that we like using OCaml, and these technical aspects aren't unique but certainly benefits we see.
Functional programming is immutable by default. TypeScript and many other typed languages don't really stop you from clobbering things, particularly with concurrency. Rust does. But immutability with GC is a lot easier to use than Rust if you don't need the performance of Rust.
That is only true since people started equating FP with Haskell.
OCaml as the discussion subject on this thread, allows for mutable data structures, and I am old enough to have been taught Lisp as one possible avenue for FP.
But what is functional besides haskell? Purescript? Elm I guess. Ocaml is not. It has for loops even. You can write pure functional ocaml but people don't. It mattered a lot less when it didn't have true concurrency, but now clobbering things in ocaml is quite possible.
OCaml has a much stronger type system than Typescript.
The real question is "why not Rust?". I've used both a fair bit and OCaml's only major advantage IMO is compile time. That doesn't seem compelling enough to put up with the downsides to me.
I'm the CTO of Terrateam. For "why not rust", I have found the downsides of Rust not compelling enough to use it. We don't need close-to-metal performance. We don't really need the borrow checker, a GC is fine. We are immutable by default so the borrow checker doesn't help much there.
I don’t like OCaml myself, but I’d pick it over rust here as bare metal perf is not necessary, and time wasting fighting the borrow checker is just not worth it.
Answer is easy, not everyone needs the performance boost provided by borrow checker, 99% of the time some kind of automatic resource management is good enough.
We like OCaml, it makes us excited to build. We know the language deeply, which means we can reason about performance and behavior before we run the code. We can onboard new engineers quickly because the type system forces clarity.
The runtime is simple enough that we can predict what it's doing. So yes, part of it is that OCaml feels good to use. But that feeling comes from years of watching it make complex systems simpler to reason about, not harder.
True but I don't think this is the correct way to frame it, because it sounds unprofessional.
The correct, and understandable way is:
- the language has properties that fit the software
- the language has properties that fit the development process
- it boosts morale of development team
- the team has the required skillset
Then it's not only a preference, but a conscious engineering choice made of evaluating the different pros and cons of various alternatives.
I would just like to distinguish "subjective and preference-based" from "social and aesthetic" and also clarify some notions.
1. The social is objective. We are social animals. It is essential to what it means to be human. We need social relations to grow and develop and to become more human.
2. The aesthetic is objective. We confuse taste with beauty, and this is perhaps the legacy of influence of certain philosophical traditions on our thinking. Beauty has to do with the fullness with which some thing instantiates a form and realizes some end/good. So, when it comes to artifacts like programming languages, a beautiful language will satisfy some human purpose more perfectly than a less beautiful language. Taste is a matter of subjective disposition to beauty. Someone with bad or poor taste might prefer the inferior over the superior, for example, or fail to discern between the two.
We sort of create mystery about preference here, as if they were just arbitrary, immutable, inexplicable brute facts. But preferences can be more good or less good or even bad. Note the relation between preference and taste.
> We're not robots.
3. Typically - and I do not accuse you of this - this is meant to mean that what makes us human compared to robots is that we have emotions. But it isn't that. Many animals have emotions. What makes us distinct as human beings is the intellectual and the rational, which robots (as computational instruments) are not.
4. Post hoc rationalizations may not stand behind the actual motivations, but the content of the rationalization may remain true and valid nonetheless.
As long as you steer away from those, the "good" choices are mostly interchangeable--use what you prefer.
If you're on TS and want end-to-end type safety, I recommend you validate everything with something like Superschema and Zapatos/PgTyped. Node with 100% type safety is wonderful to work with.
Now, if you have a bunch of "values" of type `t`, you know that they _only_ could have been produced by the `of_string` function, because `of_string` might be the _only_ function that ends with `-> t`. Therefore, all the values maintain the same properties enforced by the `of_string` function (similar to class constructors in OOP). With this, you can create types such as `Nonnegative.t`, `Percent.t`, `Currency.t`, `Image.t`, `ProfilePicture.t`, and parsers from another type to the newly minted type.
The compiler can help you enforce these properties by providing guardrails in the form of static compiler checks (these checks are run _before_ your code can even be compiled). If I have a value of type `Nonnegative.t`, then not only do I not need to validate that it's not negative, I also don't have to validate that it's not negative everywhere else that values of that type are used -- the validation logic is baked into the constructor. Parse, don't validate.*
Deleted Comment
So i work at an org with 1000s of terraform repos, we use the enterprise version which locks workspaces during runs etc.
everywhere else i’ve worked, we either just use some lock mechanism or only do applies from a specific branch and CI enforces they run one at a time.
My question is: who is this aimed at and what problem is it actually solving? Running terraform isn’t difficult - thousands of orgs handle it no problem - the issues I have with it with it have never been around lock contention and race conditions..
As you said, the common practice is to use locks on state to guarantee that operations don't step on each other. This works, however the cost is that if it takes 5 minutes to perform an operation, only one person can be doing an operation at a time, so if 5 devs are modifying infrastructure, the last one has to wait 25 minutes just to get back the plan, even if those 5 people are not changing overlapping resources in the state.
The way that most people deal with this is they take their infrastructure and break it up across multiple root modules, and then when those root modules, break it up again, etc.
Stategraph is solving the problem of getting all of the performance benefits of breaking up your root modules without breaking up your root modules. It dynamically determines which resources each of those 5 devs are operation on and, if the resources do not overlap, can run them in parallel.
That means Stategraph is manipulating state in a bit more sophisticated way than standard Terraform/Tofu, and we need to be careful we don't get it wrong.
Deleted Comment
Different teams want to move at difference cadences. At a certain scale splitting up things feels a little more natural (maybe I am stockholmed by prior limitations with TF though or just used to this way of operating now).
But even then, we're moving to k8s operators to orchestrate a bunch of things and moving off terraform apart from the stuff that doesn't change much (which will eventually get retired as well). Something like https://www.youtube.com/watch?v=q_-wnp9wRX0
Terraform variable management is our larger problem (now/nearterm) when we have to deploy numerous cells of infra that use the same project/TF files with different variables. Given the number of projects/layers of TF getting cell specific variables injected is meh.
Those variables are instance size, volume size, addresses, IAM policy, keys etc.
This is in the b2b saas world with over a million MAU. We've got islands of infra for data soverignty, some global cells where each cell can communicate back / host some shared services (internal data analytics, orchestration tooling, internal management tooling and the like).
That depends. There are many organizations (we talk to them) which have plans and applies that take 5 - 10s of minutes, some even close to an hour. That's a problem. We talked to one customer that a dev can make a change in the morning and depending on the week might have to wait until the next day to get their plan, and then another day to apply it, assuming there are no issues.
If you're in that position you have two options:
1. Just accept it and wait. 2. Refactor your root module to independent root modules.
(2) is what a lot of people do, but it's not cheap, that's a whole project. It's also a workflow change.
Stategraph is trying to offer a third option: if your changes don't overlap, each dev can run independently with no contention.
Even if one doesn't think contention over state is a big deal, I hope that one can agree that a solution that just removes that contention at very little cost is worth considering.
> This is also the home of the Flambda 2 optimiser
Their plan is to use OxCaml as their experimental fork and work with upstream to port features from it. Labelled tuples and immutable arrays for example landed in OCaml 5.4 but were originally from OxCaml.
How true is this in practice? I mean on the one hand sure Operation 2 doesn't seem some half modified state from Operation 1. On the other hand Operation 2 now has some stale state and makes the wrong decisions does the wrong thing because it didn't see Operation 1's changes.
EDIT: I wouldn't choose TypeScript either for this type of use case, but not for the reasons they state, that's my point
Ocaml has a top in class typesystem, a "faster than Go" compiler and (in 2025) good tooling. It allows you to say fuck it and write a while loop if you need to. Hell you can even do OOP. Also it has an incredible module system and full type inference. It also has an effect system, and good concurrency features (ocaml 5).
I cant say many other languages that has all the same features.
And OCaml excels at solving that sort of problem. OCaml and Erlang are the only two languages that I'm aware of that have a really clean way of doing this, in most other languages there is always some kind of kludge or hack to make it work and at best you're going to do something probabilistic: it seems to work, even under load, so it probably is good now. Until six weeks later on an idle Tuesday the system deadlocks and you have no idea how it happened.
TypeScript has soundness issues that OCaml does not have
• Strongly-typed data structures catch field errors at compile time
TypeScript does have this, although the guarantees are in practice weaker since libraries may have incorrect type definitions
• Type-safe SQL queries prevent schema drift before deployment
There are TypeScript libraries that offer this, so fair point!
• Immutability by default eliminates race conditions
TypeScript is not immutable by default
• PPX generates correct JSON serialization automatically
TypeScript does not have an equivalent to PPX infrastructure AFAIK. If there is, it's definitely not as widely used within the ecosystem compared to PPX for OCaml.
Edit: Downvoters care to respond?
This blog post shows the elements of OCaml that motivate us to use it. Is it complete? No. Maybe it should be more explicit that we like using OCaml, and these technical aspects aren't unique but certainly benefits we see.
OCaml as the discussion subject on this thread, allows for mutable data structures, and I am old enough to have been taught Lisp as one possible avenue for FP.
The real question is "why not Rust?". I've used both a fair bit and OCaml's only major advantage IMO is compile time. That doesn't seem compelling enough to put up with the downsides to me.