Fsharp: Ambiguous cases of (implicit?) yields

Created on 19 Oct 2019  路  11Comments  路  Source: dotnet/fsharp

Implicit yields have a lot of trouble with unit sequences.

Inconsistency 1

let g(f:unit->'a) = [ if true then f(); f() ]
let l = g id // l = [ (); () ]
let m:unit list = [ if true then id(); id() ] // needs type annotation; then m = []

The fact that l and m are different seems very wrong to me.

Breaking and incorrect changes

let n:unit list = [ if true then () else ()] // needs a type annotation??? // n = [] ???
let o:unit list = [ () ] // needs a type annotation??? // o = [ () ]

n should equal [ () ], not [], and should not need a type annotation. And o should not need a type annotation.

Inconsistency 2

let o:int list = [ () ] gives a type error, which is sensible, but incompatible with the spec for implicit yields.

Area-Compiler Feature Improvement

All 11 comments

I would really like a way to switch off implicit yields in F# as they make the language logically inconsistent, in a way that does not appear to be fixable.

I may need some help in understanding your concerns here. One section at a time:

First snippet:

let g(f:unit->'a) = [ if true then f(); f() ]
let l = g id
let m = [ if true then id (); id () ]

LangVersion to 4.6: fails to compile g and m, as expected
LangVersion to 4.7 (default):

  • m gives a value restriction unless you annotate it
  • Everything else compiles

You could argue that m should not give a value restriction.

Second snippet:

let n = [ if true then () else ()]
let o = [ () ]

LangVersion to 4.6:

  • Fails to compile the rhs of n, as expected
  • o compiles as unit list, as expected

LangVersion to 4.7:

  • n gives a value restriction unless you annotate it
  • o compiles as unit list, no type annotation is required

As with the previous example, you could argue that n should not give a value restriction. o does not require a type annotation as you've described.

This code never compiled prior to F# 4.7, so I'm not sure how this is a breaking change. Do you have an example of how this breaks code that used to compile?

Third snippet:

let o:int list = [ () ]

LangVersion to 4.6: Fails to compile with a type error, as expected
LangVersion to 4.7: Fails to compile with a type error

There's nothing in the spec that says or implies that o shouldn't give a type error. Where are you reading that?

I think there's arguably a bug in giving a value restriction, forcing you to annotate things, but I'm struggling to understand what is logically inconsistent and where a breaking change is.

I think the complaint is that it's surprising that the expression is parsed differently when the type is unit:

> let f = fun _ -> 'a' in [if true then f (); f ()] : char list;;
val it : char list = ['a'; 'a']

> let f = fun _ -> 3 in [if true then f (); f ()] : int list;;
val it : int list = [3; 3]

> let f = fun _ -> () in [if true then f (); f ()] : unit list;;
val it : unit list = []

Similarly, [if true then 3 else 3] is the same as [3], which makes sense because if true then 3 else 3 is the same as 3. But [if true then () else ()] is evaluated as the empty list [].

Sorry I don't know how to select an F# version to compile with. This did limit my testing. I assumed that [ if true then () else ()] and if true then (); ()] compiled in F# before 4.7 and equalled [()]. Some of these issues may have to do with yields and not implicit yields.

What's the easiest way to test a few lines of code with F# 4.6?

In your .fsproj file add inside the PropertyGroup element:

<LangVersion>4.6</LangVersion>

@charlesroddie Some concerns/drawbacks for the addition of a type-directed rule were mentioned in the RFC https://github.com/fsharp/fslang-design/blob/master/FSharp-4.7/FS-1069-implicit-yields.md, I believe your concerns correspond to those.

I do agree that the use of a type-directed rule is less than ideal in the language. However my decision in balance was that that it is a reasonable tradeoff in practice - the utility in dropping the yield in the case of list/seq/computation expressions for view descriptions was just too compelling.

We should probably move this to a discussion thread associated with the RFC

I think there's definitely an improvement to be made regarding the value restriction here.

Normal value:

let x = if true then () else () // Compiles as 'unit'

Making it a list with yields:

let x = [ if true then yield () else yield () ] // Compiles as 'unit list'

Removing the yields (the more natural way to "listify" the value)

let x = [ if true then () else () ] // ERROR: value restriction, inferred as 'obj list'

And since this doesn't happen with other values, this should be resolved so that the following can compile:

let x = [ if true then 3 else 3 ]
let y = [ if true then () else () ]

And y in the above should evaluate to [()], not []. I think this is more bugfix-level stuff that shouldn't necessarily involve core language design.

let x = [ if true then () else () ] // ERROR: value restriction, inferred as 'obj list'

How do you get the value restriction error? I can just compile this, though the resulting type for x is indeed wrong, it says obj list. This is with 16.3.1, so perhaps a newer version introduced a change here.

Same is true for that other snippet: let m = [ if true then id (); id () ]. It compiles without error for me, but with obj list as opposed to unit list.

In both cases, annotation with : unit list gives the proper type.

When running the code, each list is empty, but as you, I would expect this to be [()] and [();()] respectively.

Rather than opening a new issue I'd like to ask about the following code:

[ "1"; if v then "2"; "3" ];;

if v is false this returns [ "1" ]. This case seem to be ambiguous by design. The question is: Is there a single line way to return [ "1"; "3" ] instead? Braces don't work:

> [ "1"; (if v then "2"); "3" ];;

  [ "1"; (if v then "2"); "3" ];;
  --------^^^^^^^^^^^^^

stdin(9,9): error FS0001: This expression was expected to have type
    'unit'
but here has type
    'string'

@matthid, the ambiguity you mention may actually be a bug, as it is not consistent:

Seems like a viable work-around, but this will never return 3:
```f#
[1; if v then yield! [2] else yield! []; 3]

The following surprisingly throws an error that you cannot use `yield!` unless inside a list. seq or array expression?!?!?
```f#
[1; (if v then yield! [2] else yield! []); 3]

The following works like you intended and always returns 3, but using in basically makes it a compound expression (even though it is one line ;).

f# let x = if v then [2] else [] in [1; yield! x; 3;]

If the syntax were consistent, unambiguous and orthogonal, I would expect the ; to close the previous statement, and not become part of the else clause. Or, if also consistent, it would _always_ become part of the previous statement unless it is a single item, in which case yield! x; 3; would yield x and not 3. While that'd be unfortunate, at least it would be consistent with the way if is treated.

I'm rather surprised to find that:

  1. parenthesized expressions are not considered to be in the context of a list expression
  2. semi-colons do not separate whole statements within list expressions (this violates the operator precedence rules, where ; has lower precedence than if, meaning the if ought to be evaluated as a whole before ;).
  3. the statement above ending with ...else yield! []; 3] seems to violate type constraints. If yield! is applied to []; 3, this boils down to 3, on which yield! cannot be applied. I may analyze this wrong, but there seems to be something fishy going on here.

I think if we would just take the precedence rules as a guide, your statement above should be valid and 3 should always be returned. @dsyme, @cartermp: it seems like both the implicit and explicit yields wreck the precedence of ; vs if here? Solving the precedence rules as a bug would solve @matthid's example (which only leaves the parenthesized expr oddity).

Just in case the edit didn't send out notifications /cc @dsyme @cartermp

Personally I don't think it is necessarily a bug, but definitely an oddity.
I also love how [ "1"; if v then begin "2" end; "3" ];; doesn't work as expected either, at least that is consistent. Is there any verbose syntax to "end" the if?

Was this page helpful?
0 / 5 - 0 ratings