Skip to content

Allow Operators.string to be used in more places (prevent FS0670); optimize generated IL code #890

@abelbraaksma

Description

@abelbraaksma

Allow use of 'string' everywhere, and optimize generated IL

I propose:

  1. that we change ^T to 'T from the Operators.string function. This allows its usage in places where you would now get an error that it "escapes its scope" (report: Using string obj vs obj.ToString() leads to problems: the type parameter can escape its scope dotnet/fsharp#7958). Consider:

    type Either<'L, 'R> =
        | Left of 'L 
        | Right of 'R
        override this.ToString() =
            match this with
            | Left x -> string x    // not possible
            | Right x -> string x   // not possible

    this code fails to compile with:

    FS0670 This code is not sufficiently generic. The type variable ^T could not be generalized because it would escape its scope.

  2. that we optimize the generated IL. Currently, due to what can be considered a bug, regardless of input, the generated IL is huge (report: The 'string' operator produces suboptimal code when the object's type is known. dotnet/fsharp#9153 Since this is a statically-inlined function, each time you call string, it expands to this:

    object obj = 4;
    if (obj != null)
    {
        IFormattable formattable = obj as IFormattable;
        if (formattable != null)
        {
            IFormattable formattable2 = formattable;
            return formattable2.ToString(null, CultureInfo.InvariantCulture);
        }
        object obj2 = obj;
        return obj2.ToString();
    }
    return "";

    But need to be only this in most cases:

    myValue.ToString(null, CultureInfo.InvariantCulture);
  3. that we improve speed. Currently, a double null-check is done. This is unnecessary, in fact, many code paths don't need a null-check at all. It would also be nice to remove callvirt and use call instead (or a constrained callvirt), in cases where this is possible.

I was already implementing this (see dotnet/fsharp#9549), but while doing so, and discussing it with @KevinRansom, we found that there's a tension between the above requests and that not all of them can be supported for all scenarios. This is in part due to the nature of enum: it's treated as one of eight integer types.

The existing way of approaching this problem in F# is: roll your own string function (this has long been my approach in my own code, and this is easy if you don't have to deal with case-printing for enum).

Details are in a tentative RFC.

Pros and Cons

The advantages of making this adjustment to F# are:

  • Able to use string everywhere (except with refs, but that's another discussion)
  • Removal of dead code: current implementation has special paths for int, float etc that are never hit
  • Improved performance in most scenarios
  • Inlining will be done by the JIT in most cases (I tested this, this is true)
  • Much, much shorter generated IL code

The disadvantages of making this adjustment to F# are:

  • Some people may frown on losing inline
  • Code that ends with string x, where x is an input parameter, if never used will be inferred as type obj. After this change, it will remain generic (this is more of advantage than a disadvantage, I think)

Extra information

Estimated cost (XS, S, M, L, XL, XXL): XS

While researching this was more challenging than I thought, the final changes will be just a few lines, and probably some new test cases.

Related suggestions: None (but see the linked issues for the bug reports that lead to this)

Of note:

Affidavit (please submit!)

Please tick this by placing a cross in the box:

  • This is not a question (e.g. like one you might ask on stackoverflow) and I have searched stackoverflow for discussions of this issue
  • I have searched both open and closed suggestions on this site and believe this is not a duplicate
  • This is not something which has obviously "already been decided" in previous versions of F#. If you're questioning a fundamental design decision that has obviously already been taken (e.g. "Make F# untyped") then please don't submit it.

Please tick all that apply:

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions