Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

F# is 3 big things to me:

Safer threading with immutability

Safer programming with null-safety

Safer logic with precise domain modeling

The precise domain modeling is the real paradigm shift.

The whole point of static typing is to inform the compiler about your intent so that it can provide guarantees about correctness. F# makes it easy to define lots of small types that precisely model state so that you can give more responsibility to the compiler to verify your program.

I cannot overstate how important this is to maintaining correct software overtime. If you guard your touches with non F# code, you need far fewer unit tests and probably even less runtime checks because you already know if the types are right, so is the logic. And when you need to write unit tests, the functional style makes your tests very easy to write.

I have personally struggled with domain complexity in C# that i was able to model precisely in F# and have it work perfectly on the first try.



I would say it's the same with Rust and Haskell. And I agree, the superior type system of these languages is really a game changer for me and it's very hard to go back to languages missing these features.


For a while, yes. I know from experience that going the other way is a struggle as well. Habits die hard and change takes effort.

There's nothing superior about these type systems, they're simply two of the most static and rigid systems in circulation right now.

Try writing C, Lisp and Forth until it clicks. Then we can sit down and have an informed discussion about missing features.


I think a lot of it could be personality based. With C, Lisp and Forth you bash out code and run it, see if it works and then make changes. You get much faster visual feedback.

With Haskell, you need to put in a lot more up front thought. Then stuff doesn't compile and you spend ages working out why.. You can go for hours without having anything running. The cause of the errors can be quite abstract and obtuse and may seem like it is nothing to do with the actual problem you are trying to solve.

Different people just suit different styles.


Ghc haskell has a bunch of features to make type errors into runtime errors and a really good repl. You can do interactive development pretty well this way.

The way I write php isn't that different from how I write haskell, especially with phan running on save. Php actually requires a bit more thinking in advance because I can't refactor it quite as easily.


I wish more people understood this instead of believing that people who prefer the other side are wrong and bad.


Definitely.

What could also be taken into account is that I used to be up there preaching the static typing gospel. And then I gradually got fed up with the ceremonies, since they didn't pull their own weight and couldn't keep up.

It's all compromises. The claim is that in return for following rigid rules, they guarantee this and that. Sort of the same deal the state has pushed down our throats since forever. And for a while it seems reasonable, until you pull hard enough to notice the chains around your ankles.


I have spent some time loving all 3 of those languages, and also, most recently, F#.

I really do think that an ML-style type system is the better way to do domain modeling. The compiler support you get in F#, such as ensuring that your match expressions are complete, are nice. But what really makes me enjoy working that way is that (the non-OO bits of) F#'s type system makes it very easy to create domain models whose intent and inner workings are just obvious.

You're right, it's not correct to cast the differences as "missing features" - it's just different ways of doing things. And each has its own advantages and disadvantages. F#'s set of tradeoffs is just the one that most suits my tastes when I'm building LOB applications.


They are not just static and rigid, Haskell has type inference , which is what ihmo makes it superior


One of the features that makes F# great for organization is the sequential ordering of compilation units which makes it easier to understand code dependencies, and makes it awkward to create mutually recursive types (requiring them to be in the same code file separated by `and`, or defined in signature files in advanced).

This might sound like an unwanted restriction, but 9 times out of 10, having mutually recursive types is signs of a code smell. You can almost always model your problem better with a slight indirection through an interface or function, it helps avoid the need for mutation, and makes testing simpler.


I disliked the module\file order restriction when I started, but have come to appreciate it because every time it felt like a thorn in my side it turned out that the thorn was prodding me towards a better organizational structure.

I've even found that apart from enforcing better design, it can also trigger it. As an illustration, I've had a function 'f' in module 'C' that I decided made more sense in module 'A'. When I moved the function however, module 'A' would no longer compile because 'f' was dependent on a module 'B'. No problem I thought, I'll just move module 'A' below module 'B'. Whoops, now module 'B' won't compile; I didn't realize it was dependent on module 'A'. If I resist the urge to just revert everything and return the function 'f' to module 'C', and I investigate "why does 'f' feel like it belongs in module 'A' and yet placing it there introduces circularity...", often I'll discover a beneficial refactoring that I probably would not have thought of otherwise.


I mostly agree about mutually recursive types, but I really hate having to keep my files ordered in the project honestly.

It might be my least favorite thing about F# (Except that Option<T> is a class not a struct)


> Except that Option<T> is a class not a struct

There is ValueOption<T> now.

It's a mouthful and you'll still have to deal with all the standard library, or other third-party libraries, which produces and consumes Option<T>, but you can address that with some aliases and conversion operators if you want to go all-in:

   type 't voption = ValueOption<'t>
   type VOption<'t> = ValueOption<'t>

   let inline (!) opt = 
     if System.Object.ReferenceEquals(opt, null) then ValueNone else 
     match opt with None -> ValueNone | Some x -> ValueSome x

   let inline (?) vopt = 
     match vopt with ValueNone -> None | ValueSome x -> Some x;;

   // usage
   let x = !(List.tryFind (fun x -> x = 0) [1]);;
   // [<Struct>] val x : int voption = ValueNone


There is Mechanic (https://github.com/fsprojects/Mechanic), an OSS project that's meant to take away some of the pain, though I don't have any experience using it and I'm not aware of how useful it is in practice.


>I have personally struggled with domain complexity in C# that i was able to model precisely in F# and have it work perfectly on the first try.

If you're willing to provide a (simplified) example I would be very interested.


We found it quite nice for properties of objects that appear over time. Think things like Order that might or might not have delivery details. In C# you are making classes with nullable delivery timestamps, delivery person, etc. And one or two properties isn't that bad but it gets a little onerous when you start to have constraints like "these four properties are either all null or all populated". In F# it is trivial to set up a new constructor for Delivery that includes all of these properties. There is no unspoken agreement about that, you can set it in the model.


I see, although it looks like you're really after a state-machine there. Which I admit aren't the easiest to create in C#, and immutability and null-safety definitely make it easier...

Although personally I'd rather have the different states encapsulated in separate classes than a single type that encapsulates all possible states and enforces them through the constructor.


This is what F# does (define multiple classes), but each of these classes only takes one line of code.


Also, it defines them as nested classes, and marks the outer class as `sealed`, as they're closed types, and it prevents the type being extended with new cases.

C# can't do this because the compiler rejects putting `abstract` and `sealed` on the same type.


You can kind of solve this by making the constructor of the outer class private and defining all subtypes as nested types.

It's not ideal, but it works.


> There is no unspoken agreement...

Yes! That is exactly right, F# give you the tools to express your actual model with little/no ambiguity.


Why do you struggle with this in C#, especially given that you are familiar with the F# style? I don't know C#, but in Java I'd write this as:

    public class Order {
      final Optional<DeliveryDetails> delivery;

      public Order(Optional<DeliveryDetails> delivery) {
        this.delivery = delivery;
      }
    }

    public class DeliveryDetails {
      final long deliveryTimeMs;
      ...
      public DeliveryDetails (long deliveryTimeMs,...) {
        this.deliveryTimeMs = deliveryTimeMs;
        ...
      }
    }
My IDE writes most of these lines for me. I believe C# will have similar or more succinct constrcuts.


The ide maybe will write a small part of it, but you’ll have to keep reading it forever. And obviously it is modelled wrong because it is possible to have a Delivered order without delivery details, there is nothing that enforces it. Compare it with how I would write it in f#:

    type OrderId = OrderId of string
    type DeliveryTime = DeliveryTime of long
    type DeliveryDetails = { deliveryTime: DeliveryTime...}
    type Order = { id: OrderId ...}
    type DeliveredOrder = { id: OrderId, deliveryDetails: DeliveryDetails...}
Probably it is even better to define delivery time using the unit of measures and specifying it as ms. What is the difference in this way? That an order cannot ever have DeliveryDetails, while a DeliveredOrder must have DeliveryDetails. As a bonus you can’t just pass any string as an order id (for example a description) but you need to pass an actual order id. the same is true for the deliveryTime with the adddd advantage that you won’t be able to perform operations on it with a different unit of measure. In java or c# you would kill yourself if you try to do something similar and moreover you won’t have all the constraints specified here and the immutability automatically enforced.


What's the difference between this and having an Order class and a DeliveredOrder extends Order class in C#?


they said "an order cannot ever have DeliveryDetails"

so, if in a nominative subtyping situation like you suggest, a DeliveredOrder is-a Order, and so we can see that some subset of Orders CAN have DeliveryDetails. any method receiving an Order could receive a DeliveredOrder.


> any method receiving an Order could receive a DeliveredOrder.

This seems reasonable, if not outright desirable.


No, most of the time is wrong. If you have a DeliveredOrder you want to make sure that is not delivered again, so the delivery function should accept only an Order, not a DeliveredOrder. If in some different system you need a domain object that is both an Order and a DeliveredOrder than in F# you simply use an union type:

    type Order = UndeliveredOrder of UndeliveredOrder | DeliveredOrder of DeliveredOrder
And in this way you can write a function that accepts both an UndeliveredOrder and a DeliveredOrder.


perhaps, perhaps not. in the abstract there's no way to valuate it. it depends on what it means to be an Order and what it means to be a DeliveredOrder, what assumptions are made by code that receives an Order, etc.


very elegant solution. i agree it will take much more time to implement that in c#.


> i agree it will take much more time to implement that in c#.

If I am reading the code correctly, here is the Java version (with Lombok [1]):

    @Data class OrderId { final String id; }
    @Data class DeliveryTime { final long time; }
    @Data class DeliveryDetails { final DeliveryTime deliveryTime; }
    @Data class Order { final OrderId id; }
    @Data class DeliveredOrder { 
      final OrderId id; 
      final DeliveryDetails deliveryDetails;
    }
I can imagine C# also having similar expressive powers.

[1] http://jnb.ociweb.com/jnb/jnbJan2010.html. Lombok reduces the drudgery of writing some of the code in Java. Modern JVM languages like Scala and Kotlin have native constructs to express this.


If you are not writing java and you are using lombok then yes, this simple case seems covered well. I still can’t see how Lombok will help with the exhaustive pattern matching in case the order is a union type of several orders types as explained in my other comment or with avoiding mixing up ms and seconds when using a unit of measure in f# for deliveryTime. Also I’m curious how you would change just one field of an immutable object with 10s of properties in Lombok and if the resulting java code is as efficient as F# with its immutable data structures that use the copy on write semantics.


I agree that exhaustiveness check enforced by compiler is something I will miss in Java.

Persistent data structures have been implemented in Java if that's what you mean by efficient mutations to immutable structures. I can't imagine such structures being hard in any language hosted on the JVM or CLR.

F#, and ML-family languages, surely have their killer features. I am only contesting the claim that the GP made that modeling a domain is a struggle in C# when compared to F#.

Imagine if someone came on this thread and claimed that they struggle to write effectful code in F# which they have been writing in Haskell. Of course, you have counter-evidence of that in all the F# programs you have written so far! I feel the same about the inability-to-domain-modeling claim.


that's true, you can recreate it in some sense but you can't get to what F# can guarantee. A big promise of discriminated unions is the ability to make invalid state unrepresentable. Matching on the different type constructors is a fantastic way to only express coherent states.


> Matching on the different type constructors

Terminology nitpick: Pattern matching is done on value constructors (or just "constructors", but at the intersection of FP and OOP that could be confusing).

"type constructor" means something like `List` (as opposed to `List[Int]`) – a generic type that hasn't been applied to any type argument(s) yet, and will "construct" a type (like `List[int]`) when you apply it.


I don't think i can release the exact code but my case was like this.

I was writing a little program to help glue some things together in our build/release pipeline. This tool would be deployed to the build server and get invoked by the build agent. (This could have been a script, but the complexity got to be too much to keep organized)

The tool had two halfs:

- The frontend whose job was to gather up all the 'input' from CommandLine and Env vars, do some parsing, then spit out proper types/objects.

- The backend that would interpret this data and make decisions, make some API calls and maybe copy some files.

Because of the way our our software is built, we have 5 or 6 different 'flavors' of our app that needed special treatment during build. The complexity of branching on if it was a build step or a release step, the different flavors of our app and the need to deal with input data that may or may not be there got the best of me and I spent weeks making tweaks to deal with NREs at runtime because i hadn't handled some weird case.

So i trashed the tool and rebuild the front end in F#. I spent a little time making a very accurate type representation of the data model. Including defining a lot of stuff as optional and introducing a lot of discriminated unions to represent possible branches. Then i essentially just filled in the blanks (match cases) and fixed the compile errors until every case was covered and I was done. No bugs.

You see the big, big win of F# is the default path doesn't let your cheat yourself.

You MUST handle every switch/match case.

You MUST fully construct your records.

If your function may fail, you MUST use Option to express None/Null

And you MUST handle every option type as potentially None and write handling logic

When you define your data model, just be on honest about what data needs to be where and the compiler will keep you on the straight and narrow.


I really need to make time to look into F# some time, it sounds like it has the things I like in C# but more so.


You can try it out quickly and cheaply, at https://fable.io/repl/

There's no setup required, just start typing some F# code into the online playground and see it run immediately.


I love the trend of languages having easy online interpreters. tryhaskell.org is also very nice (though not quite `ghci`; for example, it doesn’t support `:t`).



There's a good example with explanation here

https://fsharpforfunandprofit.com/posts/designing-for-correc...


I’ve also really liked FsCheck, if well configured of course.

Just a shame that the university exam I took this summer was to implement binary trees and a parser...




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

Search: