Fsharp: `ByRefKind.InOut` treats as `ByRefKind.In` in SpanAction delegate

Created on 2 Nov 2018  路  3Comments  路  Source: dotnet/fsharp

This article about Span in C# contains following snippet:

string GetAsciiString(ReadOnlySequence<byte> buffer)
{
    if (buffer.IsSingleSegment)
    {
        return Encoding.ASCII.GetString(buffer.First.Span);
    }

    return string.Create((int)buffer.Length, buffer, (span, sequence) =>
    {
        foreach (var segment in sequence)
        {
            Encoding.ASCII.GetChars(segment.Span, span);

            span = span.Slice(segment.Length);
        }
    });
}

which translates to something like this in F#:

open System
open System.Text
open System.Buffers

let getAsciiString(buffer: ReadOnlySequence<byte>) =
    if buffer.IsSingleSegment then
        Encoding.ASCII.GetString buffer.First.Span
    else
        String.Create(int buffer.Length, buffer, fun span sequence ->
            for segment in sequence do
                Encoding.ASCII.GetChars(segment.Span, span) |> ignore
                let addr: byref<_> = &span
                addr <- addr.Slice(segment.Length)
        )

Code in C# compiles fine, but F# throws an error: The type ByRefKinds.InOut doesn't match the type ByRefKinds.In

This also won't work:

let addr = &span
addr <- span.Slice segment.Length

with error: byref point is readonly, so this write is not permitted

Repro steps

  1. create fsproj:
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netcoreapp2.1</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <Compile Include="Program.fs" />
  </ItemGroup>
</Project>

create Program.fs:

module Test

open System
open System.Text
open System.Buffers

let getAsciiString(buffer: ReadOnlySequence<byte>) =
    if buffer.IsSingleSegment then
        Encoding.ASCII.GetString buffer.First.Span
    else
        String.Create(int buffer.Length, buffer, fun span sequence ->
            for segment in sequence do
                Encoding.ASCII.GetChars(segment.Span, span) |> ignore
                let addr = &span
                addr <- span.Slice segment.Length
        )
  1. dotnet build

Expected behavior

It should be possible to compile C# snippet in F#

Actual behavior

Incoming span argument is InRef-like so it's not possible to write into it

Known workarounds

Use C#

Related information

Provide any related information

  • Operating system - Win10
  • .NET Core 2.1
Resolution-By Design

Most helpful comment

@Szer @cartermp It seems that the missing piece was a Roslyn optimization for local function argument mutation as of https://github.com/dotnet/corefx/issues/32563#issuecomment-435566555.

open System
open System.Buffers

module Test = begin

    open System
    open System.Text
    open System.Buffers

    let getAsciiString(buffer: ReadOnlySequence<byte>) =
        if buffer.IsSingleSegment then
            Encoding.ASCII.GetString buffer.First.Span
        else
            String.Create(int buffer.Length, buffer, fun span sequence ->
                let mutable localSpan = span
                for segment in sequence do
                    Encoding.ASCII.GetChars(segment.Span, localSpan) |> ignore
                    localSpan <- localSpan.Slice segment.Length
            )

end

[<EntryPoint>]
let main argv =
    let bytes =  "Hello World from F#!"B
    let ros = ReadOnlySequence<byte>(bytes)
    let s = Test.getAsciiString ros
    printfn "Result: %s" s
    0 // return an integer exit code

All 3 comments

Refactored a bit to show the delegate signature and delegate creation. But Span<char> is still a readonly byreflike type (even if you can mutate the span content).

The closest thing I can imagine to "modify" a readonly structs is "Evil Struct Replacement" (https://github.com/fsharp/fslang-design/issues/287#issuecomment-388555482) but this is no longer possible in F# for legitimate reasons.

module Test

open System
open System.Text
open System.Buffers

let stringF = fun (span: Span<char>) (sequence: ReadOnlySequence<byte>) ->
        for segment in sequence do
            Encoding.ASCII.GetChars(segment.Span, span) |> ignore
            //span <- span.Slice segment.Length

let stringD : SpanAction<char,ReadOnlySequence<byte>> = new SpanAction<char,ReadOnlySequence<byte>>(stringF)

let getAsciiString(buffer: ReadOnlySequence<byte>) =               
    if buffer.IsSingleSegment then
        Encoding.ASCII.GetString buffer.First.Span
    else
        String.Create(int buffer.Length, buffer, stringD)

[<EntryPoint>]
let main argv =
    let bytes = "abcdefg"B
    let span = ReadOnlySequence<byte>(bytes)
    let s = getAsciiString span
    0

The String.Create signature looks like this:

        public static string Create<TState>(int length, TState state, SpanAction<char, TState> action)
        {
            throw null;
        }

The SpanAction signature looks like this:

namespace System.Buffers
{
    public delegate void SpanAction<T, in TArg>(Span<T> span, TArg arg);
}

Related issues:
https://github.com/dotnet/corefx/issues/32563

"string.Create(int length, TState state, SpanAction action) formalizes the mutate newly allocated zero'd string prior to use pattern; while introducing safety via the Span and ensuring its not used prior to mutation."

Also:
https://msdn.microsoft.com/en-us/magazine/mt814808.aspx

"This method is implemented to allocate the string and then hand out a writable span you can write to in order to fill in the contents of the string while it鈥檚 being constructed. Note that the stack-only nature of Span is beneficial in this case, guaranteeing that the span (which refers to the string鈥檚 internal storage) will cease to exist before the string鈥檚 constructor completes, making it impossible to use the span to mutate the string after the construction is complete"

This is by design, as values are immutable by default in F#. A more direct translation:

let getAsciiString(buffer: ReadOnlySequence<byte>) =
    if buffer.IsSingleSegment then
        Encoding.ASCII.GetString buffer.First.Span
    else
        String.Create(int buffer.Length, buffer, fun span sequence ->
            for segment in sequence do
                Encoding.ASCII.GetChars(segment.Span, span) |> ignore
                span <- span.Slice(segment.Length)
        )

Yields that error message:

This value is not mutable. Consider using the mutable keyword, e.g. 'let mutable span = expression'.

Similarly, the byref code you have won't work because you can't use that as a way to get around immutability. We treat any dereference of an immutable value as an inref, which cannot be written to.

SpanAction is unfortunate, since it's explicitly designed to hand out a _writable_ span, but we do not interpret it this way. I think this may just be an impedance mismatch between the F# and C# way to do things.

I recommend a different approach to generating a string from a ReadOnlySequence<char>, as this sample is not directly translatable.

@Szer @cartermp It seems that the missing piece was a Roslyn optimization for local function argument mutation as of https://github.com/dotnet/corefx/issues/32563#issuecomment-435566555.

open System
open System.Buffers

module Test = begin

    open System
    open System.Text
    open System.Buffers

    let getAsciiString(buffer: ReadOnlySequence<byte>) =
        if buffer.IsSingleSegment then
            Encoding.ASCII.GetString buffer.First.Span
        else
            String.Create(int buffer.Length, buffer, fun span sequence ->
                let mutable localSpan = span
                for segment in sequence do
                    Encoding.ASCII.GetChars(segment.Span, localSpan) |> ignore
                    localSpan <- localSpan.Slice segment.Length
            )

end

[<EntryPoint>]
let main argv =
    let bytes =  "Hello World from F#!"B
    let ros = ReadOnlySequence<byte>(bytes)
    let s = Test.getAsciiString ros
    printfn "Result: %s" s
    0 // return an integer exit code

Was this page helpful?
0 / 5 - 0 ratings