Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ByRefKind.InOut treats as ByRefKind.In in SpanAction delegate #5856

Closed
Szer opened this issue Nov 2, 2018 · 3 comments
Closed

ByRefKind.InOut treats as ByRefKind.In in SpanAction delegate #5856

Szer opened this issue Nov 2, 2018 · 3 comments

Comments

@Szer
Copy link
Contributor

Szer commented Nov 2, 2018

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
@zpodlovics
Copy link

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" (fsharp/fslang-design#287 (comment)) 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<char, TState> 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’s being constructed. Note that the stack-only nature of Span is beneficial in this case, guaranteeing that the span (which refers to the string’s internal storage) will cease to exist before the string’s constructor completes, making it impossible to use the span to mutate the string after the construction is complete"

@cartermp
Copy link
Contributor

cartermp commented Nov 2, 2018

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.

@zpodlovics
Copy link

zpodlovics commented Nov 3, 2018

@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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants