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
<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
)
dotnet buildIt should be possible to compile C# snippet in F#
Incoming span argument is InRef-like so it's not possible to write into it
Use C#
Provide any related information
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
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
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
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.