dotnet / fsharp

The F# compiler, F# core library, F# language service, and F# tooling integration for Visual Studio
https://dotnet.microsoft.com/languages/fsharp
MIT License
3.94k stars 789 forks source link

Let measures support interlocked types #9258

Open Lanayx opened 4 years ago

Lanayx commented 4 years ago

Is your feature request related to a problem? Please describe. I'm using Fsharp.UMX everywhere I can to make primitives typesafe, so I use it like type SequenceId = int64<sequenceId> However when I face the need of using Interlocked methods I can't normally use measures

let mutable seqId = %0L
let x = Interlocked.Read(&seqId) // doesn't work

Describe the solution you'd like I'd like it to just work, e.g. implicit type conversion made and result casted back to measure

Describe alternatives you've considered There is a hacky way around provided by @Szer, which I won't use in code and rather stay without measures:

open System.Threading
open FSharp.UMX
open Microsoft.FSharp.NativeInterop

[<Measure>] type seqId

type Foo = { mutable SeqId: int<seqId> }

let foo = { SeqId = %1 }

let bar() =
    use natPtr = fixed &foo.SeqId
    let voidPtr = NativePtr.toVoidPtr natPtr
    let natPtr2 = NativePtr.ofVoidPtr<int> voidPtr
    let byRef = NativePtr.toByRef natPtr2
    Interlocked.Increment(&byRef) |> ignore
    ()

Additional context This is the commit that I had to do to revert measure

abelbraaksma commented 4 years ago

There's another workaround, that at least allows you to keep the thread-safety and atomic operation of Interlocked.Increment, but it requires changing the type to a struct with two overlapping fields. This works, because after compiling, the measure-info is erased (I mean, int<seqId> is really nothing more than just int).

[<Measure>] type seqId

[<Struct; StructLayout(LayoutKind.Explicit)>]
type Conv =
    /// This shadows the measure int<seqId>, and is kept private
    [<FieldOffset 0>] val mutable private Shadow : int

    /// This is your original measure-type
    [<FieldOffset 0>] val mutable Value : int<seqId>

    /// Creation
    new (value) = { Value = value; Shadow = value * 1<_> }

    /// More functional way of creation
    static member create value = Conv(value)

    /// By making Interlocked.Increment a member of the new type, we can keep this.Shadow private
    static member Incr (conv: byref<Conv>) = 
        Interlocked.Increment &conv.Shadow 
        |> ignore

type Foo = { mutable SeqId: Conv}

let foo = { SeqId = Conv.create 1<seqId> }

let bar() =
    Conv.Incr &foo.SeqId
    printfn "Value: %A" foo.SeqId.Value

Imo, this also essentially shows that there isn't anything really blocking the & to take an address of a measure-type, somehow the code should erase the measure. However, since & is an overridable operator, I hope this can be solved in Core somehow.

Conversely, if this is indeed allowed, and let x = 1<seqId> in &x returns the address of an int, it will allow programmers to easily erase the type information as well by just converting it back: let f (x: inref<int>) = x, then let x = 1<seqId>; y = f &x will yield int for y, not int<seqId>.