views:

145

answers:

6

I'm trying to write a method in F# that returns a new instance of a generic type based upon the type of a value passed into the method. In FSI:

 open System.Collections.Generic

 type AttributeIndex<'a>() = 
    inherit SortedDictionary<'a, HashSet<int array>>()

 let getNewIndexForValue (value: obj) : AttributeIndex<_> =
    match value with
      | :? string -> new AttributeIndex<string>()
      | :? int -> new AttributeIndex<int>()
      | :? float -> new AttributeIndex<float>()
      | :? bool -> new AttributeIndex<bool>()
      | _ -> failwith "bad value type"

 let someIndexes = [
    getNewIndexForValue 9;
    getNewIndexForValue "testString";
    getNewIndexForValue false;
    getNewIndexForValue 5.67;
 ]

 someIndexes;;

This does not compile with error

error FS0001: Type mismatch. Expecting a AttributeIndex<string>
but given a AttributeIndex<int>
The type 'string' does not match the type 'int'

I can't seem to figure out how to get an instance of Attribute with the type param based upon the type of the value parameter passed into the function. I've tried a couple other variation, but all result in the same type mismatch error. Any help would be greatly appreciated. Thanks!!

UPDATE:

Thanks for the answers. I get it now. So now I'm trying to have my 'getNewIndexForValue' return a non-generic base AttributeIndex class. I've implemented this in C# and it compiles and runs as I expect:

using System;
using System.Collections.Generic;

namespace Example {

    public class AttributeIndexBase : SortedDictionary<object, HashSet<int[]>> { }

    public class AttributeIndex<T> : AttributeIndexBase {
        public void AddToIndex(T indexValue, int[] recordKey) {
            if (!this.ContainsKey(indexValue)) {
                this.Add(indexValue, new HashSet<int[]> { recordKey });
            }
            else {
                this[indexValue].Add(recordKey);
            }
        }
    }

    class Program {
        static int Main(string[] args) {
            var intIdx = GetIndexForValue(32);
            var boolIdx = GetIndexForValue(true);
            var doubleIdx = GetIndexForValue(45.67);
            var someIndexes = new List<AttributeIndexBase> {
                intIdx,
                boolIdx,
                doubleIdx
            };
            return 0;
        }

        static AttributeIndexBase GetIndexForValue(object value) {
            switch (value.GetType().Name.ToLower()) {
                case "int32" :
                    return new AttributeIndex<int>();
                case "single" :
                    return new AttributeIndex<float>();
                case "double" :
                    return new AttributeIndex<double>();
                case "boolean" :
                    return new AttributeIndex<bool>();
                default :
                    throw new ArgumentException("The type of the value param is not allowed", "value");
            }
        }
    }
}

However, trying to port this to F# does not work:

  module example

     open System
     open System.Collections.Generic

     type AttributeIndexBase() = 
        inherit SortedDictionary<obj, HashSet<int array>>()

     type AttributeIndex<'a>() = 
        inherit AttributeIndexBase()

     let getNewIndexForValueType (value: ValueType) : AttributeIndexBase =
        match value with
           | :? int -> new AttributeIndex<int>()
           | :? float -> new AttributeIndex<float>()
           | :? bool -> new AttributeIndex<bool>()
           | _ -> failwith "bad value type"

     let someIndexes = [
        getNewIndexForValueType 9;
        getNewIndexForValueType false;
        getNewIndexForValueType 5.67;
     ]

This seems to me to be a pretty straight port (except in the F# version I'm constraining it to just ValueType), however I get error:

error FS0001: This expression was expected to have type AttributeIndexBase
but here has type AttributeIndex<int>

Does F# really just not support a cast of child to parent type like C# does?

+4  A: 

The fact that you can't come up with a generic parameter for the function return value is a red flag:

let getNewIndexForValue (value: obj) : AttributeIndex< ?? what goes here > =

The getNewIndexForValue function must choose a type for the 'a parameter; the 'a type parameter can be either string, int, float or bool, but not all at the same time.

One possibility is to introduce a non-generic AttributeIndex class that AttributeIndex<'a> inherits from, and return a plain AttributeIndex from your function. (The simplest change that make your code snippet compile would be to return obj, although I assume that's not going to be a permanent fix.)

Tim Robinson
NathanD
+2  A: 

The problem is that you are trying to get a non-generic method to return different bound generic types. This is just not possible. It's the equivalent of trying the following in C#

?WhatGoesHere? Create(object source) {
  if ( source is int) { 
    return new AttributeIndex<int>((int)source);
  } else if ( source is string ) {
    return new AttributeIndex<string>((string)source);
  }
  ...
}

Really the only valid type which can be inserted for ?WhatGoesHere? is object because it must be given a concrete type.

There are a couple of ways to make this experience better. The most straight forward is to add a non-generic AttributeIndex base class and have AttributeIndex<T> inherit from this value.

type AttributeIndex =
  member GetValue : object -> HashSet<int array>

type AttributeIndex<'a> = 
  inherit AttributeIndex
  inherit IDictionary<'a, HashSet<int array>>
JaredPar
A: 

Let's take a look at this line:

let getNewIndexForValue (value: obj) : AttributeIndex<_> =

The underscore in AttributeIndex<_> means to the compiler: Hey, figure out which type to insert. It basically just saves some keystrokes. If you return an AttributeIndex<string>, the compiler will infer _ is string, or bool or int respectively.

What you do is returning different types here. It's not about a certain type but any type to be allowed. This basically differs from Java's generic wildcards.

  • Java's List<?> is a list of any possible values (like object would do).

  • F#'s _ list is a list of exactly one type.

What you want is the Java wildcard, and under .NET, you express there through co/contravariant types.

Therefore, you need AttributeIndex<obj>.

Dario
Note that F# doesn't support variance, and even if it did, in .NET variance can only be used with interfaces and delegates.
kvb
+2  A: 

I originally posted this as a comment, but in fact it is 'the answer':

What do you intend to do with 'someIndexes' later?

Until you specify that, any 'answer' is just idle speculation. One can't provide prescriptive advice about how to get past this 'type error' because there are many possible ways to get past it depending on how you eventually intend to consume the data. I think @JaredPar's answer is the most likely to be what you want, but that's basically me trying to be psychic (along with every other answer).

Once you specify 'what you want to do' with each index, then that will imply a common interface or base class that all indexes need to support, and that will be your answer.

Brian
Hi Brian:The code above is just a small example created from a larger set of code I've already written. Essentially, what I'm creating is a generic in-memory index manager. The idea is that you pass it an object hierarchy with the requirement that each single object in the object hierarchy has an "id" field. Then, I create indexes for every string or valuetype in the object. When querying the indexes, you get back a list of int[] (representing the "id" fields for each level).I was able to do this already in C# with 3.8 million heterogeneous objects each with a hierarchy 3 levels deep.
NathanD
I'd love to share my whole code to see what you think and if I could be going about it in a completely different, more functional way. But, Stackoverflow is probably not the place to do that. If you give me a means to get my code to you I'll send it over.
NathanD
+2  A: 

Everyone else is right, but I think I can add a little more explanation.

Every expression in F# has a type. In F#, "if a then b else c" is an expression, returning either b or c. For this to work, b and c must have the same type. "match ..." is also an expression, returning the value of one of its clauses. That implies that every clause must have the same type.

Your match expression tries to violate this. Each clause has a different type. In your case, these are different applications of the same generic type... but those are still different types. This is no different than trying to have one clause return an int and another return a string...

Conceptually, that is the root of your problem. Until you fix the match expression, you won't be able to define a function that returns its value.

As others have pointed out, one of the ways to fix this is to use a common base type for the match expression's type, such as obj or a non-generic AttributeIndex.

Jason
+4  A: 

Your latest code will almost work, but F# requires you to explicitly upcast to AttributeIndexBase in this case. There are at least two ways to do this: you could use the upcast keyword or you could use the :> conversion operator.

The first option would look like this:

let getNewIndexForValueType (value: ValueType) : AttributeIndexBase =
  match value with
     | :? int -> upcast new AttributeIndex<int>()
     | :? float -> upcast new AttributeIndex<float>()
     | :? bool -> upcast AttributeIndex<bool>()
     | _ -> failwith "bad value type"

While the second would look like this:

let getNewIndexForValueType (value: ValueType) : AttributeIndexBase =
  match value with
     | :? int -> new AttributeIndex<int>() :> _
     | :? float -> new AttributeIndex<float>() :> _
     | :? bool -> new AttributeIndex<bool>() :> _
     | _ -> failwith "bad value type"
kvb
Kvb, thank you!! I love you!!Both of those solutions work like a charm. It seems so obvious now.
NathanD
+1 for two things I hadn't seen before: upcast or :> to underscore.
Jason