With out the hash sign (#) a type means it must be exactly that type with a hash sign any type that inherits from that type is okay. As seq<'a> is actually the interface IEnumerable effectively this means when you use seq<'a> without the hash sign then you must always downcast to this type.

I think an illustration may help here:

1
2
3
4
5
6
#light
let fun1 (s : seq<'a>) = Seq.iter (printfn "%A")
let fun2 (s : #seq<'a>) = Seq.iter (printfn "%A")

fun1 ([1; 2; 3] :> seq<int>)
fun2 [1; 2; 3]

The call to fun1 would not type check with out the downcast becase the parameter is of type list<int> and not type seq<int>, but fun2 does not require any downcast.

Hope that helps,
Rob

By on 1/6/2008 12:22 AM ()

Hi Robert,

Thanks, that does explain it. I thought it was something subtle. You couldn't do that in C# could you?

Dave

By on 1/6/2008 12:37 AM ()

Yes its very subtle indeed. No there's no really equilient in C#, in C# all type annotations behave like the #seq<'a> case. I believe the difference comes from the fact that the build in types in F# tuples/records/unions have no hierarchy - you can't create a record that inherits form another record, so there's no need for anything other than an exact match.

By on 1/6/2008 1:18 AM ()

It is very subtle and an important point about F# from a programming language design point-of-view. In C# and other OO languages, actual parameters at a call site are implicitly upcast to match the signature of the callee. This is very natural from an OO perspective and OO programmers just expect this without thinking about it.

In F# a design decision was made not to allow implicit upcasting. I believe the main reason for this is to facilitate type inference -- without this, type inference would become impossible or too difficult -- but Don could give the authorative answer.

N.B. that the hash notation is only syntactic sugar for convenience. The hash in the function above

1
val fun2 : #seq<'a> -> unit

is actually a short way of writing

1
val fun2 : 'b -> unit when 'b :> seq<'a>

No implicit upcasting and this style of generic parametricity can also be more expressive than implicit upcasting. This thread also covers the issue [link:cs.hubfs.net].

By on 1/6/2008 2:47 AM ()

N.B. that the hash notation is only syntactic sugar for convenience. The hash in the function above

1
val fun2 : #seq<'a> -> unit

is actually a short way of writing

1
val fun2 : 'b -> unit when 'b :> seq<'a>

On one hand I understand the two notations are equivalent, on the other hand I think their usage in F# is somehow confusing. I found that

  • The result of type inference of a value will always give in the first notation
  • But when declaring a parameterized type, you should always use the second notation

Example:

1
2
3
4
5
6
7
8
9
10
(* type inference: the first notation *)
> let f x =  Seq.iter ignore x;;
val f : #seq<'b> -> unit
(* a few attemptions to define type with the first notation, all fail *)
> type 'a t when 'a = #seq<int> = 'a -> unit
> type 'a t = #seq<int> as 'a -> unit
> type 'a t = 'a -> unit  when 'a = #seq<int>
> ...... 
(* The way is to use the second notation *)
> type 'a t when 'a :> seq<int> = 'a -> unit

Why not just unify them and provide a single notation?

By on 1/6/2008 5:59 AM ()

Yes, I can see how this can be confusing. The parametric notation (like fun2) is the fundamental notation that can be used everywhere. The question is in what places can you use the hash notation (like fun1) short-cut?

You can use it in signatures of functions, and a function signature is always printed in hash notation because it is clearer.

You've discovered that you can't use has notation in a type declaration. I guess the reason is that the hash notation implicitly introduces extra type variables to its context. This is fine in a function declaration because you don't need to see the type variables because you they are handled by type inference. E.g., in the declaration of

1
let fun2 (s : #seq<_>) = Seq.iter (printfn "%A") s

you never declare if the function fun2 has any type arguments, nor do you care if it has since the passing of type parameters is handled behind-the-scenes by type inference.

However in a type declaration, you must explicitly declare all type arguments because there is no type inference occurring. The hash notation cannot add a new type variable to the type argument list when it has been explicitly declared because the programmer has specified that the existing type variables in the list are the only ones allowed. The same situation arises if you try to use hash notation in a function with explicit type arguments. E.g., you cannot write this

1
let fun2<'a> (s : #seq<_>) = Seq.iter (printfn "%A") s

So I guess a rule could be you cannot use hash notation in the presence of an explictly defined type parameter list.

By on 1/6/2008 8:54 PM ()
IntelliFactory Offices Copyright (c) 2011-2012 IntelliFactory. All rights reserved.
Home | Products | Consulting | Trainings | Blogs | Jobs | Contact Us | Terms of Use | Privacy Policy | Cookie Policy
Built with WebSharper