Reputation: 93
I think I do not quite understand how F# infers types in sequence expressions and why types are not correctly recognized even if I specify the type of the elements directly from "seq".
In the following F# code we have a base class A and two derived classes, B and C:
type A(x) =
member a.X = x
type B(x) =
inherit A(x)
type C(x) =
inherit A(x)
If I try to "yield" their instances in a simple sequence expressions, I get two errors:
// Doesn't work, but it makes sense.
let testSeq = seq {
yield A(0)
yield B(1) // Error, expected type: A
yield C(2) // Error, expected type: A
}
That can make sense, since it may not be so trivial to infer "common" types (interfaces, I think, can make that work far harder). However, those errors can be fixed with a safe cast:
// Works fine :)
let testSeqWithCast = seq {
yield A(0)
yield B(1) :> A
yield C(2) :> A
}
What if I do not want to use casts? I tried to specify the sequence type directly from "seq", but things do not seem to work:
// Should work, I think...
let testGenSeq = seq<A> {
yield A(0)
yield B(1) // Error, expected type: A
yield C(2)
}
So, my question is: is there a way to avoid casts? If not, is there a reason why even specifying the type doesn't make the code work?
I tried digging through following links:
http://msdn.microsoft.com/en-us/library/dd233209.aspx http://lorgonblog.wordpress.com/2009/10/25/overview-of-type-inference-in-f/
But I found nothing useful...
Thank you in advance for any kind of answer you can give :)
Upvotes: 9
Views: 629
Reputation: 93
This is just a summary of all answers my question has received, so that future readers can save their time by reading this (and decide whether or not reading other answers to get a better insight).
The short answer to my question, as pointed out by @Gene Belitski, is no, it's not possible to avoid casts in the scenario I described. First of all, the documentation itself states that:
A sequence is a logical series of elements all of one type.
Moreover, in a situation like the next one:
type Base(x) =
member b.X = x
type Derived1(x) =
inherit Base(x)
type Derived2(x) =
inherit Base(x)
We surely have that an instance of Derived1
or of Derived2
is also an instance of Base
, but it is also true that those instances are also instances of obj
. Therefore, in the following example:
let testSeq = seq {
yield Base(0)
yield Derived1(1) // Base or obj?
yield Derived2(2) // Base or obj?
}
We have that, as explained by @Gene Belitski, the compiler cannot choose the right ancestor between Base
and obj
. Such decision can be helped with casts, as in the following code:
let testBaseSeq = seq<Base> {
yield Base(0)
yield upcast Derived1(1)
yield upcast Derived2(2)
}
let testObjSeq = seq<obj> {
yield Base(0) :> obj
yield Derived1(1) :> obj
yield Derived2(2) :> obj
}
However, there's more to explain. As @kvb states, the reason this can't work without casts is that we are implicitly mixing generics, inheritance, and type inference, which may not work together as well as expected. The snippet in which testSeq
appears is automagically transformed in:
let testSeq = Seq.append (Seq.singleton (Base(0)))
(Seq.append (Seq.singleton (Derived1(1)))
(Seq.singleton (Derived2(2))))
The issue lies in Seq.singleton
, where an automatic upcast would be needed (like in Seq.singleton (Derived1(1))
) but it cannot be done since Seq.singleton
is generic. If the signature of Seq.singleton
had been, for example, Base -> Base seq
, then everything would have worked.
@Marcus proposes a solution to my question which boils down to define my own sequence builder. I tried writing the following builder:
type gsec<'a>() =
member x.Yield(item: 'a) = Seq.singleton item
member x.Combine(left, right) = Seq.append left right
member x.Delay(fn: unit -> seq<'a>) = fn()
And the simple example I posted seems to be working fine:
type AnotherType(y) =
member at.Y = y
let baseSeq = new gsec<Base>()
let result = baseSeq {
yield Base(1) // Ok
yield Derived1(2) // Ok
yield Derived2(3) // Ok
yield AnotherType(4) // Error, as it should
yield 5 // Error, as it should
}
I also tried extending the custom builder so that it supported more complex constructs like for
and while
, but I failed trying writing the while handler. That may be a useful research direction, if someone is interested in.
Thanks to everyone who answered :)
Upvotes: 0
Reputation: 55184
This is a good question, and the answer is probably more complicated than the responses you've gotten so far indicate. For instance, this does work:
let l : A list = [A(0); B(1); C(2)]
but this seemingly analogous code doesn't:
let s : A seq = seq { yield A(0); yield B(1); yield C(2) }
The reason is actually very subtle. The second case desugars to something which is basically a more complicated version of:
let s : A seq =
Seq.append (Seq.singleton (A(0)))
(Seq.append (Seq.singleton (B(1)))
(Seq.singleton (C(2)))))
So what's the problem? Ultimately, the problem is that Seq.singleton
has generic type 'x -> 'x seq
, but we want to pass a B
and get back an A seq
in the second call (by implicitly upcasting the instance). F# will implicitly upcast a function input of one concrete type to a concrete base type (e.g. if Seq.singleton
had signature A -> A seq
we could pass a B
!). Unfortunately, this doesn't happen with generic functions (generics, inheritance, and type inference don't play nicely together).
Upvotes: 7
Reputation: 10350
In order to understand the cause of your confusion you should not go anywhere further, than the first statement of the link you referred to :
A sequence is a logical series of elements all of one type.
You can return a sequence of only one, the same type like seq<A>
, or seq<obj>
. The OOP-ish fact that types B
and C
are inherited from A
is not relevant. The following may help: all your instances are also inherited from obj
, but in order to make from them a seq<obj>
you should explicitly cast:
// Works fine
let testSeq = seq<obj> {
yield A(0) :> obj
yield B(1) :> obj
yield C(2) :> obj
}
or just box
them like below:
// Works fine too
let testSeq = seq {
yield box (A(0))
yield box (B(1))
yield box (C(2))
}
EDIT: For understanding the reasoning behind explicit casting in F# the following (simplistic) consideration may help. Type inference does not do guessing; unless it can derive seq
type deterministically, or have it explicitly declared, it will complain.
If you just do
let testSeq = seq {
yield A(0)
yield B(1)
yield C(2)
}
compiler is presented with indeterminism - testSeq
can be either seq<A>
, or seq<obj>
, so it complains. When you do
let testSeq = seq {
yield A(0)
yield upcast B(1)
yield upcast C(2)
}
it infers testSeq
as seq<A>
based on type of the first member and upcasts B and C to A
without complaining. Similarly, if you do
let testSeq = seq {
yield box A(0)
yield upcast B(1)
yield upcast C(2)
}
it will infer testSeq
as seq<obj>
based on the type of the first member upcasting this time second and third members to obj
, not A
.
Upvotes: 6
Reputation: 6117
The asker has already accepted an answer, however the following may be useful. On the issue of "is there a way to avoid casts" I would like to add: using strictly seq
the answer is as already given (not possible).
However you could write your own "workflow". Something like:
open Microsoft.FSharp.Collections;
let s = seq<string>
type A(x) =
member a.X = x
type B(x) =
inherit A(x)
type C(x) =
inherit A(x)
type MySeq<'a>() =
member this.Yield(item: 'a): seq<'a> =
Seq.singleton item
member this.Yield(item: 'b): seq<'a> =
Seq.singleton ((item :> obj) :?> 'a)
member this.Combine(left, right) : seq<'a> =
Seq.append left right
member this.Delay (fn: unit -> seq<'a>) = fn()
[<EntryPoint>]
let main argv =
let myseq = new MySeq<A>()
let result = myseq {
yield A(1)
yield B(2)
}
0
Note that this answer is not particularly compile time safe, not quite sure if that can be done (pesky generic constraints).
Upvotes: 2
Reputation: 61
There is no implicit upcasting in F# check here. You can try inferred upcasting.
let testSeq : seq<A> = seq {
yield A(0)
yield upcast B(1)
yield upcast C(2)
}
Or if it is enough you can use discriminated unions:
type X =
| A of int
| B of int
| C of int
let testSeq = seq {
yield A 0
yield B 1
yield C 2
}
Upvotes: 3