Open En3Tho opened 2 years ago
Would the methods like "Trim()" be lifted? What would they return?
That is the problem - it's not just a type, it's a type with operations. We don't get far without defining how those operations are transformed. It's the same problem with allowing UoM on string
Well, personally I think that we shouldn't really touch operations at all, e.g. let Trim() return the same thing as aliased type does. But the key difference will be this:
type UserName = [<StrictAlias>] string
let bob: UserName = "Bob"
let alice: UserName = "Alice"
bob.Replace("o", alice) // <- doesn't work as it's a strict alias, you have to explicitly convert strict alias to string
let alice: string = alice // <- explicit convert from aliased type to base
bob.Replace("o", alice) // now works
E.g. strict aliased types coudn't just be interchanged with each other or base type without explicitness.
I only talked about UoM for reference, in fact I believe UoM has completely different idea (eg. safe calculations, UoM combinations e.t.c.). Here it's more about call correctness/strictness.
you have to explicitly convert strict alias to string
If we need to be explicit, why is it better than this?
[<Struct>]
type UserName = UserName of string
At least with that it becomes clear how to convert in/out of the type, and
The reasons in favour of making the representations iddentical would be things like "it looks better in the debugger" or "it looks like a string to reflection". But do you really want to be able to cheat the system as easily as this?
let x : UserName = "a"
box x :?> string
Well, my problem with DU's usually is that they require having "stuff" around them: serialization, implementing interfaces, e.g. full decoration if you need to mimic the type they wrap.
The boxing example can already cheat existring TypeAliases and UoMs. The same thing with Unchecked. I do not feel like having a battle with those features at compile time because they can't really be fought this way..
I guess the scope of this proposal is to have a way to introduce more type corretness at very little expense.
But at the same time I understand that all this explicitness talk might result and a very similar way of wrapping and uwrapping things, basically like those DU's do. And it's not clear without better examples/better syntax how this would be THAT MUCH better.
you have to explicitly convert strict alias to string
If we need to be explicit, why is it better than this?
[<Struct>] type UserName = UserName of string
At least with that it becomes clear how to convert in/out of the type, and
I think the issue with that is the much higher friction getting values out of the wrapper type. E..g taking the Replace
example from above you can end up with something like:
[<Struct>]
type UserName = UserName of string
let bob = UserName "Bob"
let alice = UserName "Alice"
let newStr =
let (UserName str) = bob in str.Replace("o", let (UserName str) = alice in str)
Which contrasted with a way to just cast in and out which might look something like:
type UserName = [<StrictAlias>] string
let bob: UserName = "Bob"
let alice: UserName = "Alice"
let newStr = (bob :> str).Replace("o", alice :> str)
I've used the DU struct type a lot and the safety is worth it, but it would be nice to reduce the friction getting single values out. Off the top of my head would an attribute to expose Value
work?
[<Struct; ValueProperty>]
type UserName = UserName of string
let bob = UserName "Bob"
let alice = UserName "Alice"
let newStr = bob.Value.Replace("o", alice.Value)
Obviously only valid on single case DU's with a value.
Rather than use a struct single case discriminated union, you could instead use a struct single field record.
[<Struct>]
type UserName = {
Value: string
}
let bob = { UserName.Value = "Bob" }
let alice = { UserName.Value = "Alice" }
let newStr = bob.Value.Replace("o", alice.Value)
SharpLab.io appears to indicate that a struct single case discriminated union results in effectively identical enough generated code as a struct single field record.
The overall request seems to be a way to define a type that gives one word injection and .Value
projection
UserName x
x |> UserName
x.Value
and maybe a pattern too
UserName x
It's an interesting reasonable request. We could realistically safely provide an automatic .Value
if one is not provided already for this:
[<Struct>]
type UserName = UserName of string
which then gives you what you want, though it's still a little too magical/implicit for my liking given that member x.Value = (let (UserName v) = x in v)
is fairly short.
The other thing is that auto-generated properties for single-case unions are also reasonable, so maybe
[<Struct; AutoProperties>]
type UserName = UserName of value: string
Could give x.Value
if we're happy to implicitly capitalize (ugh...). That would then apply to multi-data single case unions too:
[<Struct; AutoProperties>]
type UserName = UserName of value: string * name: string
@dsyme With #752,
member x.Value = (let (UserName v) = x in v)
can be
member (UserName v).Value = v
@dsyme Hmm. Just a thought. Could compiler just issue an optional warning when a different aliased type is used at a call site?
I don't particularly like standardizing on .Value
; it's too easy to break if adding or removing an option
wrapper for this type. If you consume .Value
in a weakly typed manned (.ToString()
, sprintf "%O"
, obj
, etc.), the code still compiles if you add/remove an option
wrapper since it too has a .Value
property, but the behavior is now incorrect. I've been bitten by this in the past enough times that I no longer call the property .Value
.
I more-or-less always use explicit conversion (either op_explicit
, .To<target-type>()
, or both) for accessing the underlying value of a wrapped primitive. This avoids the issue @cmeeren mentions. It also encourages consumers to think of the type as its own nominal "thing" (so, stronger modeling than a simple type abbreviation). In fact, I'd say any time you don't want an abbreviation and it's source type to be interchangeable, you really shouldn't use an abbreviation. So, this strikes me as being more like Haskell's newtype
feature, which has been dismissed in the past. So it may be worth revisiting the whys and wherefores.
I know that @En3Tho mentioned it and I agree that they're a hack, but personally I use FSharp.UMX and I'm pretty happy.
In addition to open FSharp.UMX
I use the new open type FSharp.UMX.UMX
statement. This way I can explicitly the tag
and untag
methods instead of the usual 'jolly' operator %
, which can be confusing (it can both tag and untag implicitly, it cannot have type parameters, and it's difficult to search for).
I recommend anybody interested in a similar syntax try it, as it's really close to what this proposal is aiming for. At the very least it should give useful experience for the purpose of designing a built-in, universal feature.
@Frassle's proposed syntax was:
type UserName = [<StrictAlias>] string
let bob: UserName = "Bob"
let alice: UserName = "Alice"
let newStr = (bob :> str).Replace("o", alice :> str)
With UMX, it looks like:
open type FSharp.UMX.UMX
type [<Measure>] user
// optionally:
// type UserName = string<user>
let bob = tag<user> "Bob"
let alice = tag<user> "Alice"
let newStr = (untag bob).Replace("o", untag alice)
Often when modeling domain cleanly or trying to refactor some legacy code, I (and my friends / collegues) use type aliases to bring more "context" to function/type w.e. signature
The problem is that compiler still doesn't help with reducing mistakes this way: eg programmer is actually on his own. Aliased type still can be interchanged when consumed: eg.
Compiler already has a great way to help in similar case: Units of Measure, eg.
int<kg>
cannot be mistaken withint<sec>
from compiler's point of view.I'm proposing an Attribute which will tell compiler to see those types as something like base type + hidden unit of measure. Something like:
I think casting syntax isn't ideal here. But it still solves my problem at least. It needs to be given much more thought, to make it explicit but at the same time not too boilerplaitish I guess.
The existing way of approaching this problem in F# is well, be careful about it. Also, there is UMX, but it doesn't cover all cases and I do not think that units of measure are really great fit here. They were designed for a different thing after all. Some people use Single case DU for this but I think they still have this boilerplate problem. Also, forgetting to put
[<Struct>]
attribute can potentially lead to huge increase in allocations.Pros and Cons
The advantages of making this adjustment to F# are ...
I think the main advantage is type safety. Reducing the chance of using invalid type.
The disadvantages of making this adjustment to F# are ...
Not like disadvantages but more like what this proposal is not trying to solve: This doesn't make the code any safer to outside consumers eg. C# People still have to remember that this is just an alias, strict or not, e.g. registering an aliased type in DI will result in registering base type etc.
Extra information
Estimated cost (XS, S, M, L, XL, XXL):
I think it's something around M, as compiler already has infrastructure for this feature.
Affidavit (please submit!)
Please tick this by placing a cross in the box:
Please tick all that apply:
For Readers
If you would like to see this issue implemented, please click the :+1: emoji on this issue. These counts are used to generally order the suggestions by engagement.