Hacker News new | past | comments | ask | show | jobs | submit login

I get the "Make illegal states unrepresentable" in theory but how do you do it actually for nontrivial preconditions like "this list must be sorted"? (as a precond for a binary search on a vector, for example).

OOP is fine with functional if you make it immutable too, so I don't see the problem (I've not really got a problem with mutable OOP, or state generally, if it's done carefully).




> how do you do it actually for nontrivial preconditions like "this list must be sorted"?

This depends on your data model, of course. But for a list of integers you could do the following:

1. Have a unique data type that is the list of Deltas, plus the initial element (So for instance the list [5, 3, 6] would be encoded as (3, [2, 1]).

2. Provide a sort function that creates such a sorted list.

3. Make the sorted list your input parameter.

This is obviously oversimplified, but I hope you get the idea.

A cheaper alternative is to

1. Make an opaque datatype "sorted list", together with a function that translates this type to a normal list. Implemen this type just as a list, but keep the implementation private.

2. Provide a function sort, that is the only function that can yield that type.

3. Demand that type as input.


Encapsulation aided by types and module interfaces.

In Reason/OCaml, you can create a module interface (.rei or .mli) which makes the type opaque to functions outside the module. So instead of saying `type t('a) = list('a)`, it'll just say `type t`.

The compiler relies on this type information to decide whether functions are well-typed. So if an external module presumes to know the structure of the type and does an operation on it, it becomes a type error because the compiler simply doesn't know about the actual type. This is similar to encapsulation in OO where we don't expose a field to the outside world, but here it is done by virtue of the type signature itself, which I found to be a more powerful guarantee of encapsulation.

The module can then expose functions like `append` which would be the only way you can manipulate the list. This function in-turn can ensure the postcondition, guaranteeing that the list stays sorted. At the point in which we want to use the list for functions not supported by our SortedList module, we can turn it into a regular list with a `to_list` function. Since the underlying type is already a list, the operation is virtually free. The function would look like `let to_list = (xs: t('a)) => (xs: list('a))`. It is an identity function, with just the type changed for the compiler.

Similar rules about `append` applies to the constructor: we can create a new `SortedList` only with a `SortedList.make` which can ensure the postcondition. There will be no other way, thanks to the type being hidden, to create a value of the `SortedList.t` type.


I would use the “smart constructor” pattern for this: https://draptik.github.io/posts/2020/02/10/fsharp-smart-cons...

Things like that require runtime logic, there’s no way around that.


It's idiomatic to use "opaque types" to achieve this in F#. In practice it is actually pretty analogous to the concept of a "constructor" in OOP.


The difference between doing functional programming in C# vs doing functional programming in F# is subtle but important. A true FP language like F# will not allow code that introduces certain classes of errors FP is designed to prevent. Whereas in C#, a developer needs to be really careful not to introduce those errors even when following FP style. C# will always allow mutable states, null values, global variables, to name a few.


As other replies to this comment describe, we make heavy use of opaque types where runtime enforcement of safety is need. Some examples: PositiveFloat, PositiveDecimal, NonEmptyMap, NonEmptySet, NonEmptyList, KeyedSet.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: