9 ms·
C# and F# approaches to illegal states
- deanCommie 11y agoIt would require some type checking and casting (or runtime exceptions) but wouldn't the C# equivalent to the F# approach be abstract class User, class ActiveUser : User, and class DeletedUser : User
- drabiega 11y agoI believe this is what the author is referring to at the end: >Unfortunately, the lack of discriminated unions and pattern matching makes the F#’s approach mostly unbearable to implement in C#. It’s possible in some simple cases but still remains pretty tedious.
- inglor 11y agoGood point. No, I don't believe that's what they mean there. The OP defines the F# version user as a union of either an activeuser or a deleteduser. The C# idiomatic way would be to use an abstract class and implement two subclasses. The difference is that in the C# way someone could in theory (if they're your evil wizard arch nemesis for example) create a third user subclass and harm the type soundness. In practice you can guard against that as well if you really have to guard against evil wizards.
- pionar 11y agoThe article is pointing out (I think) that since C# and F# are such different paradigms, they have different best practices for handling invalid states. F# has these best practices built in to its type system. The way the author is coding the domain models, there's no way for the model to be in an invalid state. C#, on the other hand, does not give this safety in its type system (and doesn't make it easy to "abuse" the type system to provide it). One can use contracts[0] to do this, but it provides mainly runtime checking, not compile-time checking like F#. [0]http://www.dotnetcurry.com/csharp/1172/code-contracts-csharp-static-runtime-checks http://www.dotnetcurry.com/csharp/1172/code-contracts-csharp...
- mariusmg 11y ago"The technique C# traditionally uses to deal with illegal states is called design by contract" That's not really true. Code contracts are just a library , it's certainly not "traditional" in C# to use that to check for invalid state. Traditionally you just check the value before assignment. And if you do this your entire problem "goes away". Gotta say this is one of the most contrived "F# is better than C#" example i ever saw.
- inglor 11y agoThis. The nullable field and an enum is hardly the standard way to do things in C#. That's like having NULLable fields everywhere in SQL. Assignment checking is pretty simple but would not work if you want to verify the correctness of the program in compile time. Which you can still do just fine in C#. You would use a class hirarchy with a `User` abstract class and `ActiveUser` and `DeletedUser` subclasses and use the type system to enforce the constraints. The syntax wouldn't even look worse.
- koyote 11y agoHow would you go about implementing activeUser.Delete()? I guess you would have to replace the object and return a new DeletedUser and then make sure that your activeUser object is not referenced anywhere else?
- inglor 11y ago> How would you go about implementing activeUser.Delete()? I guess you would have to replace the object. If we're talking about a functional style then yes, `User`s would be immutable object, changing them would return a new user. Doing `activeUser.Delete()` returns a new deleted user, doing `deletedUser.Undelete()` returns a new active user. I might go as far as creating the user as a value type (struct) but that's a bit unorthodox.
- mannykannot 11y ago> Doing `activeUser.Delete()` returns a new deleted user... But don't you now risk having two mutually contradictory objects representing one user?
- fsloth 11y agoI don't think there's anything stopping from implementing ActiveUser and DeletedUser as separate classes in C# as well? It's much more succinct in F# but so is pretty much everything else.
- alkonaut 11y agoThe F# code is no doubt more elegant because of discriminated unions and pattern matching, but the C# code in this comparison is a bit lazy/sloppy if you ask me. I know it's a made up example to showcase discriminated unions, but it becomes a bit of a strawman to suggest that C# doesn't even allow representing discriminated unions. It does: through inheritance and tons of boilerplate code. The correct demo I think would have been to make the correct type (which doesn't allow invalid states to be represented) in both languages, to show how in C# it required maybe hundreds of lines, and in F# it's just a few lines. Under the hood, almost exactly the same types are created from F#, where the F# types when seen in C# or IL of course look very much like the types you had to meticulously type in C#.
- DanielBMarkham 11y agoBut no. The point was to show how when you're thinking in a language that supports algebraic types and pattern-matching, you automatically exclude states that can't exist. It's not that the underlying CLR or IL is any different given the same solution -- that's just measuring a language's verbosity. It's that some language constructs automatically lead you down the path to correct code without having to boil the ocean. Sidebar: I would have tightened the F# code up a bit. No need to have the Name string repeated in both types.
- inglor 11y agoOr, you could use a class Hirarchy with a User abstract class and ActiveUser and DeletedUser subclasses and use the type system to enforce the constraints. Using a nullable field and an enum is hardly the standard way in C#.
- nbevans 11y agoBut unfortunately it is the one a lot of C#'ers reach for because it is the smallest amount of code in that language and often results in the least refactoring effort. A properly designed model in C# like suggested is a large number of lines of code. And then because there is no pattern matching, you have to hand-roll your type checks/casts and make sure have that method throw an unconditional exception at the end for literally impossible states. Then if you make a change to this model, i.e. you add a new "SuspendedUser" subclass, you have to make sure you go through the codebase to update all those places you wrote a hand-rolled "pattern match" against that type. Because the compiler won't warn you about an unrepresented state on your type checks. And so then you have to make sure the whole project team is aware and you have to make sure new hires are familiar with the patterns being used. So yeah, the problems just snowball and then you wonder why you didn't just use a more appropriate language in the first place that has already taken complete care of all these trivial programming issues.
- bad_user 11y agoWhile the solution described is cool, it only applies to problems that can be solved with union types in F#. That's cool, but things get much more complicated when trying to describe the concept of a non-empty list (still possible, but I don't believe is included in F#'s library), or near impossible when trying to describe things like a number within a certain range, or various relationships between the arguments of a function or of a type. I mean, when you're constructing something like "Segment(x, y)" (or "Segment of double * double" in F#, not sure if I got the syntax right), how do you express that the X coordinate needs to be lower than the Y coordinate? And then the biggest modern challenge is to model state machines for asynchronous communications, which implies working in the presence of non-determinism. Good luck modeling that with a static type system. Basically there are things that can be expressed in a statically typed way, by means of a good enough type-system, but there are always invariants that can only be checked at runtime. Hence assertions sprinkled in the code are still valuable even in a language like F#, because if you're going to fail, it's better to fail as fast as possible.
- josteink 11y ago> And then the biggest modern challenge is to model state machines for asynchronous communications, which implies working in the presence of non-determinism. Good luck modeling that with a static type system. I don't think async really matters at type-level. What matters is the failure-mode. And Haskell seems to be doing this just fine, by abstracting IO into its own aspect which can either succeed or fail, and thus separates the domain-model from the failure-modes. It's by no means impossible, and it's pretty defeatist to assume we all have to end up in some sort of dynamic non-typed nodejs-like blob as soon as you invoke Async or network operations. Even C# handles this quite well, and fully typed all the way.
- bad_user 11y agoI wasn't referring to the Async/Task in C# / Futures or Promises in other languages. That model is too simplistic and can only be applied to a limited set of problems. Much more interesting is to model actors / components that talk with multiple other actors / components concurrently and whose state and consequently the set of messages it can receive or emit is changing over time, depending on the state of the components it communicates with. In Erlang's actor model it's not defeatist to say that the set of all possible messages an actor can receive can be as useful as Any or String in the face of an ever changing context, coupled with the fact that actors can expose different interfaces depending on to whom they talk to. Imagine a component that on one side receives signal readings determining if some physical asset is available or not and on the other side it accepts commands for that asset, but only if the asset is determined to be available. And the commands are numbers in a certain range that also varies over time depending on the signal readings. And as a more recent example of where static typing fails are the transducers from Clojure. Same problem, as in ... modeling of state machines. > And Haskell seems to be doing this just fine, by abstracting IO into its own aspect which can either succeed or fail If you are talking about the I/O monad, I believe that the I/O monad sucks for doing actual I/O, though it's pretty cool for in-memory stuff. This is because it has baked-in the assumption that those operations are synchronous. Ironically it's the same big problem when you look at, say, the common Iterable pattern and compare it with monads. And I'm not up to date with latest developments, but Iteratees also suck. BTW, I'm a big fan of static typing. I remember a time where I had a hard to reproduce bug that I couldn't understand and I fixed it by eliminating the possibility of it happening through the type system, which was sort of a revelation. But as with everything, there are limits in what you can express and I also believe in runtime constructs where it makes sense.
- huhtenberg 11y ago> Design by contract Back in the day we used to call it an assert, sprinkled it over code to cover the invariants and generally didn't make much fuss about it. Kids these days... "design by contract"... such words, much wow :-P
- agumonkey 11y agoI never knew how to feel about it. Eiffel was centered around contracts, surely there must something more to it than `assert`.
- rbehrends 11y agoDesign by Contract isn't just asserts. The only thing that contracts and asserts have in common is that they are expressed as semantic predicates. DbC formalizes the idea of a contract (hence the name) between the caller and callee (in Eiffel parlance, the client and supplier of a service) and what each expects of the other and is part of the interface of a class. Accordingly, there are rules for contracts (visibility of symbols used in a contract must match the visibility of the function it is attached to, how contracts are related to each other via inheritance, etc.). It is not a terribly complicated concept, but it's also not just asserts.
- gnaritas 11y ago> Design by Contract isn't just asserts. That's exactly what it is, but it's a very specific style of asserts with very specific rules as to what and when to assert; it is however, still just a bunch of asserts.
- platz 11y agoyep!
- rbehrends 11y agoOkay, then explain to me how (say) in C++ you can annotate pure virtual methods with contracts or how inheritance of contracts can be done in C++, assuming you want to implement them using asserts. For that matter, how do you implement class invariants, which need to be checked before and after each qualified call to an instance of the class, but not when the call is unqualified? The other essential part that asserts don't really capture is that contracts (as pre- and postconditions) are part of the API, while asserts are part of the implementation.
- rwmj 11y agoBe good to also compare this to SQL databases like PostgreSQL, where you can have quite complex check constraints, non-nullable columns and unique indexes. If done properly it can make it literally impossible to insert bad data into your database.
- deleted 11y ago[deleted]
- agumonkey 11y agoIsn't it related to phantom types ?
- sklivvz1971 11y agoIt's a contrived example. There is no need to store the "state" of a user if you can infer it from other state. Just define a "deleted" user as a user with a deletion date... Modeling is hard, maybe it should be done more accurately...
- alkonaut 11y agoAnd even if it was required (Say there were 3 states) it's an obvious smell to have a field in a type that is used only in one "state" of the object. The presence of such a field is the most obvious clue that one needs a new class for that state.
- achr2 11y agoPersonally, I would call it a smell to have classes that only extend minor state, especially when this forces immutability into paradigms that do not have immutability as a prime component (.net UI frameworks for example). To me IsActive/Deleted is a property of a User, not a type of user.
- alkonaut 11y agoIn this contrived example, the deletedDate alone is enough to create the required states (Deleted being when there is a deletedDate) without creating any invalid date (Deleted without deleted date or vice versa). In general though, if there were 3 states (active, suspended, deleted) where some states required more data than others, I'd probably make a struct+enum to create the equivalent of the F# type. The user can expose the state while hiding the fact that it has a more complex internal field.
- jameshart 11y agoThat's far from a universal truth. Only US users have a 'social security number' field. Should US users be a different class than German users? Only cars have a 'passenger count' field. Only electric vehicles have a 'battery capacity' field. What type system can accommodate both Teslas and Harley Davidson LiveWire bikes?
- brudgers 11y agoWe can depict this solution in the following way: [table labeled :Users:] In my opinion that's a better way of encoding the desired behavior than either the C# or F# code by several orders of magnitude. It can be read to find out what privileges a user state allows. It can be read to find out what user states is required to access a privilege. The key is that tables are readable. By anyone. Even non-programmers. All the behavior is right there. The question about what's the best way to represent it in code is not F# versus C#. It's automatically versus wrong. The table should be the point of truth. [1] [1]: The point of departure is Aaron Brooks' idea of live specification. I don't claim to accurately represent his view. Listen for yourself here: http://blog.cognitect.com/cognicast/074 http://blog.cognitect.com/cognicast/074
- thomasz 11y agoI don't get it. It is perfectly valid to use immutability and the Type system to enforce those invariants in c#. abstract class User { public string Name { get; } abstract bool IsActive { get; } public User(string name) { this.Name = name; } } class ActiveUser : User { public ActiveUser(string name) : base(name) { } private ICollection<Subscription> _subscriptions = new List<Subscription>() public IEnumerable<Subscription> Subscriptions => _subscriptions; IsActive => true; public DeletedUser Delete() => new DeletedUser(Name, DateTime.Now); public void AddSubscription(Subscription s) => Subscriptions.Add(subscription); } class DeletedUser : User { public DeletedUser(string name) : base(name) { } public DateTime DeletionDate {get;} IsActive => false; public DeletedUser(string name, DateTime deletionDate) : base(name) { this.DeletionDate = deletionDate; } public ActiveUser Reactivate() => new ActiveUser(Name); }
- vkhorikov 11y agoYou are right, inheritance is perfectly applicable to the example from the article, basically because the example is very simple. The idea is that in more complex scenarios the best practice is to prefer composition over inheritance because modelling every single piece of state with subclasses would be sloppy and unmaintainable. That is where design-by-contract (or simply put guard clauses) usually come into play.
- kvb 11y agoBut someone else could create a subclass breaking the invariants. It's possible to avoid this by making the abstract class's constructor private and nesting the two subclasses inside of the abstract class, but this is not idiomatic. If you have to work that hard then most people won't bother, while in F# it's very natural to create such a design.
- thomasz 11y agoI do not understand how you want to break the invariants by subclassing. My ActiveUser can be deleted and my DeletedUser can be reactivated. How do you break that?
- nowprovision 11y agoGranted the F# approach is somewhat nicer, but I don't think the C# approach with contracts is too bad (no recent experience with .Net - 6yrs ago contracts were painful to use), but you also could go the immutable route and via a method call easily return a new instance of User and enforce the invariant prior to creating a new instance etc.. It's a minor issue in the end, the bigger issue is that your client/stakeholder knows what they actually want and you actually capture those in-variants exhaustively and correctly (implementation is minor, F#/ocaml/haskell can't save you here)...
- romaniv 11y agoAFAIK, code contracts in .NET still have all the issues they had several years ago. The absolute worst one being that you need to install something for them to actually work. This means different computers will have different code behavior.
- verinus 11y agoIn my opinion and from the comments I think that encoding one state is not a problem either in C# or F#. Now assume we have to add an additional state to a user and all proposed subclassing solutions in C# turn out to be what they are: not optimal. My biggest problem with OO is, that it only allows us to structure code in a one dimensional way (inheritance) but in reality we will face more complex scenarios. and even this way is increasingly discouraged: composition over inheritance...
- achr2 11y agoC# certainly has other options to straight inheritance like explicit interfaces and extension methods.
- jameshart 11y agoAnd now you've baked your understanding of reality into your type system or your invariant contracts, you come to write some code that actually works with this API and realize you've made a terrible mistake, because when you delete a user, you need to know which subscriptions to cancel, but you've already thrown that information away as soon as you called 'Delete()'. Maybe we should restrict you from deleting a user while they have active subscriptions? Well, that's easily accomplished with a C# invariant, a little harder to bake into the F# Type union. In general, though, it turns out you often need to be able to have objects in an 'illegal' state some of the time. "Zip code is a mandatory field" - but that doesn't mean you can't save your order form for later if you need to go and look up the zip code. It's generally best to validate objects as being 'eligible' for a particular operation, not to prevent them from being ineligible altogether - An object with no zipcode can be saved, but it can't be submitted. Structuring that sort of thing into a type system - while doable - is probably not idiomatic in either F# or C#.
- ionforce 11y agoIf you're experiencing some sort of mismatch with your model, it means that your model is incorrect.
- jameshart 11y agoBut if your model is churning every time your understanding of the business rules changes, then you have a different problem.
- noblethrasher 11y agoYour model will usually be incorrect because in all but the most trivial applications the model will be the result of experimentation. Even if you “accurately”[1] model the business rules, those rules themselves are subject to change as the business revives it’s model of the market, or customer relationship, or regulatory environment, or whatever. The main selling point of extreme late-binding (e.g. ad-hoc polymorphism, dependency injections, message passing, etc) is that lets you quickly revise your model. [1] Whatever that means.
- thoth 11y agoI think folks are lost in the weeds of criticizing the specifics of the example code and not paying attention to the larger design message - that being F# provides language constructs that make it easier to avoid allowing illegal states, and the type system will help detect that during compilation. Yes it is a contrived example, yes you can write "better" C#, but this is a short example for illustrative purposes. I bet if somebody wrote a blog post about Djikstra's letter "GOTO considered harmful" 45+ years ago, attempting to illustrate it with more examples, it would have collected similar comments: nothing wrong with GOTO, I can use it correctly; that example of spaghetti GOTO code is poor and not idiomatic; the example code is too simple and nobody would write it that way; yes it takes discipline to use GOTO without ill effects but nobody would accidentally/maliciously subvert my GOTO hierarchy; etc. Meanwhile, the original point about structuring code using different techniques to help avoid problems (and avoid GOTO) would be lost. It's like people are overly defensive about language features (or lack thereof) as if writing extra boilerplate and not letting the compiler help is a badge of honor. EDIT: clarity
- thomasz 11y agoI think that is highly questionable. Sum Types are a different approach with some advantages and some disadvantages over inheritance. Nothing more and nothing less. They really shine if you have clearly defined types that seldom change. On the other hand, discriminated unions make it absurdly complicated to defend against some errors that are almost comically easy to avoid by using a standard OO approach: class Range { public int From {get;} public int To {get;} public Range(int from, int to) { Assert(from <= to); From = from; To = to; } }
- kvb 11y agoYour particular example doesn't seem especially object oriented (e.g. it looks like a simple data type with a "smart constructor", which is a common pattern in, say, Haskell, too). But in any case F# supports both discriminated unions as well as normal OO design, while C# supports only the latter, so in that subset of cases that are better modeled by DUs F# will be strictly better, and in the rest of the cases it won't be worse.
- mannykannot 11y agoThis is all very well for the single binary state of the example, but as the number of states increases, do you not get a combinatorial explosion in the number of types and state-transition functions?
- eropple 11y agoThis is my concern with F# for sure (and I like F#, though I haven't used it in anger sufficiently). When using Scala, I had to minimize the times I did this sort of thing, usually across a single axis rather than the multiple ones I'd want to do it across. Which doesn't mean I dislike the whole concept, but it's way more limited than this article wants you to think.
- wehadfun 11y agoWhat would be the approach if user had another state such as RestrictionLevel {admin, normal}. Would the we make a subsclass for every combination between actibe and RestrictionLevel?