Preferences

F# is a chimera of a language. The functional parts of the language are nicely designed: no nulls, discriminated unions (ADTs), you write simple functions in simple modules and there is nothing that is too clever to understand: it's very pragmatic. Then there is a whole lot of stuff like inheritance, classes, interfaces, nulls mainly there for dotnet interoperability that gets very ugly very fast. There are way too many variants of the same thing: records, classes, struct records, tuples, struct tuples etc, which are mainly there either for compatibility with similar c# stuff or because the default language constructs often result in suboptimal code. At the end I went with Rust because it has one way of doing such stuff. And for those interested in a gc language with functional features there is now Gleam

C# will eventually have unions that will undoubtedly be incompatible too, I worry about source generators becoming ubiquitous as well.

This was also my experience with F#, phenomenal language dragged down by ugly interop with an ecosystem that barely acknowledges its existence and I feel is incompatible with its ideals.

Shame too because there’s some genuinely great stuff in the community like fable [1] where if you were to chuck in JSX like templating you’d have an absolute killer web tool rather than the mess blazor is.

It’s ironic that I found js interop less annoying than .net interop.

[1] https://github.com/fable-compiler/Fable

> There are way too many variants of the same thing: records, classes, struct records, tuples, struct tuples etc, [...]

> At the end I went with Rust because it has one way of doing such stuff.

I haven't looked at C# in 20 years, but Rust certainly has a LOT of ways to do similar things too:

    struct Foo { x: f64, y: f64 }          // Struct
    struct Foo(f64, f64)                   // Tuple Struct
    let f = (1.0, 2.0)                     // Tuple
    enum U { Foo(f64, f64) }               // Enum Tuple
    enum V { Foo { x: f64, y: f64 } }      // Enum Struct
    [ 1.0, 2.0 ]                           // Size-2 Array
    &[ 1.0, 2.0 ]                          // Slice
    vec![1.0, 2.0]                         // Vec
Then possibly wrapping those in Rc, Arc, Gc, Box, Cow, Option, Result, RefCell, RefMut, Cell, OnceCell, LazyCell, UnsafeCell, Weak, and so on... that's a multiplicative product of possibilities. And you still need raw pointers for interop with C libraries.

Anyways, I haven't used Rust in a few years now either, and I'm sure I've made some mistakes and omissions above, but I don't remember it as the poster child for Python's "There should be one-- and preferably only one --obvious way to do it".

> struct Foo { x: f64, y: f64 } // Struct struct Foo(f64, f64) // Tuple Struct let f = (1.0, 2.0) // Tuple enum U { Foo(f64, f64) } // Enum Tuple enum V { Foo { x: f64, y: f64 } } // Enum Struct [ 1.0, 2.0 ] // Size-2 Array &[ 1.0, 2.0 ] // Slice vec![1.0, 2.0] // Vec

I don't know, I'm not too bothered by this. If you take struct tuple and struct, Yes synctacticqlly they are different but functionally they are quite similar. You have values lying in memory. There's no reason to prefer one to the other except convenience. Program behaviour will not change. This is not the the case for fsharp classes and records and structs. Same way Fsharp has two types of lists. It also has an array type and all these are different but they look the same. In rust case they all have different names but behaviour wise they are same: data stored in contiguous memory with some differences in what you can do with them. I do like fsharp as a language but it can't go full on its promises because of dotnet. One glaring one is nulls. The language itself claims to be null safe. Except if you use dotnet types like strings. Then the compiler doesn't warn you. Honestly Kotlin does this better with non nullable types and null chaining

Yeah, having the multiple "record" types in Rust doesn't bother me either. I just disagreed that the situation is clearly simpler than it is in F#.

If you think of using C# libraries from F# as comparable to using other people's crates in Rust, you're going to get exposed to other people's choices for data structures in both.

It’s not so much that you’ve made any mistakes or omissions as much as all of these things do different, similar things, but don’t do the exact same thing.

For example, an array and a tuple are both aggregate types, but arrays store multiple value of a single type, and tuples store multiple values of the same type.

Some of these do boil down to “named or anonymous” but that’s also two different things.

More power to you for defending or explaining Rust, but the context of the conversation is comparing multiple "record" types in C# as "bad" to the "one way" in Rust. It's hard to argue that Rust has a simpler story than C#.

There are a lot of almost orthogonal features one might choose for a record type:

Accessor: rec.x, rec.0, rec[0], match

Constant vs Mutable

Reference vs Value

Nominal vs Structural/Anonymous typing

Subtyping for inheritance

Subtyping for sum/union types

And more depending on the language (ownership in Rust)

> tuples store multiple values of the same type.

I'm sure this was a typo. :-)

It was a typo, thanks :)

I’m not trying to pass judgement on C#. I just don’t see a lot of these things as being that similar to each other.

Heh, I just remembered that Rust has like 6 or 7 ways to do strings too.
Rust has one string type in the language, and three more in the standard library. This boils down to the intersection of “owned vs borrowed” and “Rust native strings vs C native strings,” and you only need the C variants when doing FFI.
I'm (basically) aware of the details (String, &str, OsString, OsStr, CString, CStr, "star" c_char, and probably some others ("star" const i8, &[u8], ???), and you and I have had this conversation a while back when I had a stronger interest in Rust. I'm not sure if you're correcting me, but you're basically confirming what I said.

As for only needing them when you need them, how could it be otherwise? :-)

One thing I would say is if you're writing a normal Rust application or library and do not care about c interoperability, you could get by without being aware of anything other than the first 2 types. However in Fsharp you are forced to learn about all other ways of doing things, plus how C# does things, because it is almost impossible to do anything useful without interoperating with C# and dotnet
Well there are the 3 you mentioned (records, classes, and tuples) which should be easy enough to differentiate from each other. The struct versions aren't necessary to use in most cases, and are an optimization.

The thoughtful, but not breakneck speed of changes within the language is one thing I appreciate a lot. Things do get added (there are proposals and discussions that are fairly regular in the GitHub repo for language design matters). A recent example is adding a spread operator.

Yeah those examples I gave are not the best. But why records and classes when records can also have methods. What I was getting at was the language looks good until you dive deeper and get into all those rough edges of dotnet interoperability. An example I can think of is functions/methods. I think F# style is to write curried functions (no brackets for function inputs), except class methods are written mostly non curried. Computation expressions are non curried also even though that is an F# only features. Then there are two ways of writing generics: C# reified generics and rust like monomorphised generics (with inline keyword) and they used to have two different syntaxes until recently
Yeah typically if you're exposing F# code out to C# as a library, you'd want to keep the external API utilizing features that have better interoperability (classes), whereas code that's written in F# and only expected to be called by F# can use things like currying and such.

In practice, this ends up being mostly simple to deal with.

In the other direction, consuming C# libraries historically hasn't had too much trouble other than they don't really design them with any functional-leaning in mind. The real problem that's growing recently is the dotnet teams move towards C#-centric features. Things like source-generators, roslyn, etc that are "C# features" and not "dotnet features". These types of things could create a big enough rift to break practical usage of F# as a dotnet interoping language if it goes unchecked.

> But why records and classes when records can also have methods.

Records have built in equality logic, Classes don't.

And, because of the interop story, they wanted to be able to define classes.

> C# reified generics and rust like monomorphised generics (with inline keyword) and they used to have two different syntaxes until recently

I almost prefer the old way, where SRTPs required the other syntax. On the other hand I guess between 'inline' and constraints the compiler makes the 'best choice' now...

Also, just minor pedantic comment, both C# and F# will monomorphize for struct of T

> Records have built in equality logic, Classes don't.

I'm aware of this but i feel this is confusing. Just putting () changes semantics a whole lot

> Also, just minor pedantic comment, both C# and F# will monomorphize for struct of T

I wasn't aware thanks

You can choose which constructs you use. Avoiding classes in F#, while possible, would be a mistake as they are a way to hide data and functions where these are not intended to be part of the public API. F# is outstandingly pragmatic and the struct types for performance have been a nice addition. As a beginner you can ignore those if you are not interested in getting max perf.

Dotnet does have some minor annoyances (exceptions being one) but gives the ability to deploy everywhere, GUI apps on all platforms, web, backend is fantastic. Lack of native GUI apps would be the main deficiency for me from Gleam, plus the fact that they have decided their community is explicitly politically woke-liberal.

Rust has been a fantastic addition to the language ecosystem, but recursive types, which are the bread and butter of ML-family code, are just too hard to write in Rust.

This item has no comments currently.

Keyboard Shortcuts

Story Lists

j
Next story
k
Previous story
Shift+j
Last story
Shift+k
First story
o Enter
Go to story URL
c
Go to comments
u
Go to author

Navigation

Shift+t
Go to top stories
Shift+n
Go to new stories
Shift+b
Go to best stories
Shift+a
Go to Ask HN
Shift+s
Go to Show HN

Miscellaneous

?
Show this modal