What the article calls "loci" satisfy the axioms of an affine space, with the exception (for example in the case of pointer arithmetic) that the base set might not be a field but only a ring, and therefore it would be a "pseudo-affine" space based on a module rather than a vector space.
Programming by specifying algebraic properties is very easy and natural, but in most languages it's a pain to encode in the type system.
This is all pretty interesting. I definitely like how methodical this all is. I'd be willing to try some of this practically to see where I chafe with it!
Considering the Amount domain types, I like the manifold project’s unit expression[1] approach where the unit and domain type are integrated and always reconciled.
… there's a number of (rather trivial?) fixes to some of the problems they hit.
First, you can have a type-checker check the currency; that doesn't have to be a runtime error:
Money<Usd> m = …;
There will, of course, be instances where you must handle currency dynamically.
"MoneyExpr" is a bit more complicated; I find most people usually implement "MoneyBag" intuitively first, so it's odd they left that for third? So, the article gets there, and we "fix" the problems, but it feels roundabout.
The criticism of MoneyBag is bizarre:
> Compared to the Martin Fowler’s design, it remains heavier. It consumes more memory and more CPU
This is apples to oranges? Fowler's design is a Money type; MoneyBag is a different thing; of course it consumes more memory. A Vec<int> consumes more memory than an int, too?
Perhaps the article author isn't comfortable with that Money merely isn't closed over addition. But if you accept that Money isn't closed over addition, then naturally there must be another type in the type system.
I'm not sure what the confusion is, the question being explored is, "how do you define a Money type that features the operations we typically expect to perform and with the results having the expected properties?". He reviews various well-known published designs and their drawbacks, and outlines a design that does not have those drawbacks.
> I find most people usually implement "MoneyBag" intuitively first, so it's odd they left that for third?
> First, you can have a type-checker check the currency; that doesn't have to be a runtime error:
> Money<Usd> m = …;
> There will, of course, be instances where you must handle currency dynamically.
With dependent types, checking for currency validity may be handled at compile-time. (This is touched upon in the post linked in the comment you replied to.)
For the case of conversion between currencies of monetary amounts:
Although I disagree with some of the details, I think this type of approach to domain modeling is some of the most valuable work software engineers can do; in fact it's the type of analysis and rigor that justifies the word "engineer" in the title of "software engineer". But I do have two important points to add:
1. Don't restrict yourself to "common" types when you're thinking this way -- all of domain modeling can be approached this way. Working on a Pizza Ordering app? Think about the properties of various operations in your domain and how they are related to mathematical structures. Are they commutative, well-orderable, equatable, multiplicative, scalar, groups/rings/fields, monoids? Are there changes you can make that would allow your operations to have desirable properties? This stuff genuinely does pay off down the road when you want to expand your ideas or re-use your code on something else.
But be careful: not all useful mathematical structures have names or show up in textbooks! The goal is not to map your domain to things that mathematicians have named, it's just to understand it at this level (and get your code to understand it at this level too). If you find yourself arguing over whether it's actually a vector space, a tensor space, or an affine space, then stop worrying about external definitions and re-focus on the properties of your domain. Arguments about how your domain works are good; arguments about exactly what other mathematicians have written are bad.
2. This post is focused on getting the compiler to understand these concepts, but there's still a lot of value in explicitly modeling them in code even if it's "runtime checked" instead of compile-time checked. Many of these techniques end up requiring higher-kinded types to really get the compiler on board, which unfortunately most languages today don't support. But that doesn't mean you should change the abstraction, it just means you get a lower (or later) level of checking.
Really great and interesting blog post! The different categories of domain types identified all seem useful. It's clear how practical these ideas can be when implementing or thinking about domain-specific types. I'll definitely be keeping these terms in mind for future programming.
> Identifiers have no structure, i.e., we don’t care about their internal representation. The only fundamental requirement is the ability to compare values of those types for equality. This lack of structure suggests an appropriate mathematical model for such types: a set, a collection of distinct objects.
I don't understand what this is trying to say and this "mathematical model" of sets doesn't seem to be applied further on. In math, objects are usually always assumed to have a notion of equality, so I don't see how that alone implies a set. A mathematical abstraction that matches the `Eq` trait would be a setoid.
The `LocusLike` trait is a lot like a metric space with a total order. I wonder if it can be implemented by things that are not basically numbers under the hood (not that it needs to, just curious)?
> Unlike design patterns, the universal domain types are based on mathematical abstractions and can be specified precisely.
I feel like this is over-selling it a bit. I think the ideas are great but, at least as presented here, I don't think they are rigorous enough to deserve the label "mathematical abstractions".
> I don't understand what this is trying to say and this "mathematical model" of sets doesn't seem to be applied further on.
I think what is meant by it is that we have a set without additional structure. Other structures like vector spaces, groups, fields, etc. are (at least in mainstream maths) also sets, but they're equipped with structure - operations and axioms that govern how these operations work. But if you have just a set, you can't assume anything about the internal structure of what's in that set.
It's a really awful name. Is primitive obsession a good code smell or a bad code smell? And if code smell is a bad thing, and if primitive obsession is a good thing, wouldn't the code smell be lack of primitive obsession?
I don't mean to be pedantic. But it's quite exhausting trying to parse the meaning out of "primitive obsession is a code smell". It took me a while to figure out whether the author considered "primitive obsession" to be a good thing or a bad thing, not least because the they provided an example but not a definition.
The term "code smell" only ever has a negative connotation. The title of the article is also "Primitive Obsession — A Code Smell that Hurts People the Most" -- doesn't seem too difficult to parse...
But the solution being advanced is to prevent inheritance of primitive operators. Implying that Primitive Obsession is a good thing. Unless the author means "primitive type obsession". I imagine they did.
If you appreciate the patterns from DDD and the idea of domain modeling with type systems, I highly recommend checking out "Domain Modeling Made Functional" by Scott Wlaschin.
I recently watched a talk he gave with the same title. It was excellent. Honestly having one of those planning/discovery meetings before any code gets written can prevent so many nasty problems.
29 comments
[ 3.3 ms ] story [ 69.8 ms ] threadProgramming by specifying algebraic properties is very easy and natural, but in most languages it's a pain to encode in the type system.
1. https://github.com/manifold-systems/manifold/tree/master/man...
https://deque.blog/2017/08/17/a-study-of-4-money-class-desig...
First, you can have a type-checker check the currency; that doesn't have to be a runtime error:
There will, of course, be instances where you must handle currency dynamically."MoneyExpr" is a bit more complicated; I find most people usually implement "MoneyBag" intuitively first, so it's odd they left that for third? So, the article gets there, and we "fix" the problems, but it feels roundabout.
The criticism of MoneyBag is bizarre:
> Compared to the Martin Fowler’s design, it remains heavier. It consumes more memory and more CPU
This is apples to oranges? Fowler's design is a Money type; MoneyBag is a different thing; of course it consumes more memory. A Vec<int> consumes more memory than an int, too?
Perhaps the article author isn't comfortable with that Money merely isn't closed over addition. But if you accept that Money isn't closed over addition, then naturally there must be another type in the type system.
> I find most people usually implement "MoneyBag" intuitively first, so it's odd they left that for third?
I have not found that at all.
With dependent types, checking for currency validity may be handled at compile-time. (This is touched upon in the post linked in the comment you replied to.)
For the case of conversion between currencies of monetary amounts:
https://github.com/anderslundstedt/type-experiments#type-saf...
1. Don't restrict yourself to "common" types when you're thinking this way -- all of domain modeling can be approached this way. Working on a Pizza Ordering app? Think about the properties of various operations in your domain and how they are related to mathematical structures. Are they commutative, well-orderable, equatable, multiplicative, scalar, groups/rings/fields, monoids? Are there changes you can make that would allow your operations to have desirable properties? This stuff genuinely does pay off down the road when you want to expand your ideas or re-use your code on something else.
But be careful: not all useful mathematical structures have names or show up in textbooks! The goal is not to map your domain to things that mathematicians have named, it's just to understand it at this level (and get your code to understand it at this level too). If you find yourself arguing over whether it's actually a vector space, a tensor space, or an affine space, then stop worrying about external definitions and re-focus on the properties of your domain. Arguments about how your domain works are good; arguments about exactly what other mathematicians have written are bad.
2. This post is focused on getting the compiler to understand these concepts, but there's still a lot of value in explicitly modeling them in code even if it's "runtime checked" instead of compile-time checked. Many of these techniques end up requiring higher-kinded types to really get the compiler on board, which unfortunately most languages today don't support. But that doesn't mean you should change the abstraction, it just means you get a lower (or later) level of checking.
> Identifiers have no structure, i.e., we don’t care about their internal representation. The only fundamental requirement is the ability to compare values of those types for equality. This lack of structure suggests an appropriate mathematical model for such types: a set, a collection of distinct objects.
I don't understand what this is trying to say and this "mathematical model" of sets doesn't seem to be applied further on. In math, objects are usually always assumed to have a notion of equality, so I don't see how that alone implies a set. A mathematical abstraction that matches the `Eq` trait would be a setoid.
The `LocusLike` trait is a lot like a metric space with a total order. I wonder if it can be implemented by things that are not basically numbers under the hood (not that it needs to, just curious)?
> Unlike design patterns, the universal domain types are based on mathematical abstractions and can be specified precisely.
I feel like this is over-selling it a bit. I think the ideas are great but, at least as presented here, I don't think they are rigorous enough to deserve the label "mathematical abstractions".
I think what is meant by it is that we have a set without additional structure. Other structures like vector spaces, groups, fields, etc. are (at least in mainstream maths) also sets, but they're equipped with structure - operations and axioms that govern how these operations work. But if you have just a set, you can't assume anything about the internal structure of what's in that set.
[0]: https://medium.com/the-sixt-india-blog/primitive-obsession-c...
[0] https://martinfowler.com/books/refactoring.html
I don't mean to be pedantic. But it's quite exhausting trying to parse the meaning out of "primitive obsession is a code smell". It took me a while to figure out whether the author considered "primitive obsession" to be a good thing or a bad thing, not least because the they provided an example but not a definition.
...Why don't we just import he rest of algebra here :)