views:

237

answers:

2

Trying to learn some f# and I've run into a couple of hangups.

Here's the code:

#light
module HtmlModule

type HtmlString(text:string) =
    override x.ToString() = text

type HtmlAttribute(key:string, value:string) =
    inherit HtmlString(key + "=\"" + value + "\"");

type HtmlElement(tag: string, ?contents:list<'a> when 'a :> HtmlString) =
    inherit HtmlString("")
    let innerContents = defaultArg contents []
    let castToHtmlAttribute (hs:HtmlString) : Option<HtmlAttribute> =
        match hs with
        | :? HtmlAttribute as temp -> Some(temp)
        | _ -> None
    member x.tag = tag
    member x.contents = innerContents |> List.filter(fun x -> (castToHtmlAttribute x).IsNone)
    member x.attributes = innerContents |> List.map(fun x -> (castToHtmlAttribute x)) |> List.filter(fun x-> x.IsSome) |> List.map(fun x->x.Value)

    override x.ToString() =
        let sb = System.Text.StringBuilder()
        sb.Append("<" + x.tag)

        for att in x.attributes do
            sb.Append(" " + att.ToString())

        sb.Append(">")

        for item in x.contents do
            sb.Append(item.ToString())

        sb.Append("</" + x.tag + ">")
        sb.ToString()



let element tag contents = new HtmlElement(tag, contents)
let div contents = element "div" contents
let p contents = element "p" contents
let text text = new HtmlString(text)
let attr key value = new HtmlAttribute(key, value)
let classes value = attr "class" value

and a quick console program to show the problem:

#light

open HtmlModule


let stuff = [for a in 1 .. 10 do yield p [text ("some String " + a.ToString())]]

let elem = div [
            attr "class" "someClass";
            text "This is some inner text";
            div stuff;
        ]

let result = elem.ToString()
printfn "%A"  result

the compiler will not allow the the line

div stuff;

it states "Error 7 Type mismatch. Expecting a HtmlString list but given a HtmlElement list The type 'HtmlString' does not match the type 'HtmlElement'"

This problem can be tracked to the "castToHtmlAttribute" method signature

 let castToHtmlAttribute (hs:HtmlString) : Option<HtmlAttribute> =

if I make this line more generic like so

let castToHtmlAttribute (hs:'a when 'a :> HtmlString) : Option<HtmlAttribute> =

I get a new error, "Error 2 This runtime coercion or type test from type 'a to HtmlAttribute involves an indeterminate type based on information prior to this program point. Runtime type tests are not allowed on some types. Further type annotations are needed."

Any help is appreciated.

+1  A: 

Expecting a HtmlString list but given a HtmlElement list The type 'HtmlString' does not match the type 'HtmlElement'"

This states the problem quite well. For F#, a List<HTMLElement> is not a List<HTMLString> like a List<Orange> is no List<Fruit>.

If it were, you could write

Apple :: oranges

Since this doesn't make any sense, F# expects you to cast downcast explicitly. Just tell div to work with a generic list.

In short, you can change the type HTMLString list to a polymorphic #HTMLString list or cast the arguments or stuff by hand.

Dario
+3  A: 

The diagnostic here is not great.

You can fix the code like so:

let stuff = [for a in 1 .. 10 do 
               yield (p [text ("some String " + a.ToString())]) :> HtmlString]

where I've inserted an explicit upcast to the base type.

Brian
I'm really going for more of an api, and would rather client code not have to cast.
justin
Do consider Juliet's advice. Overall it's hard in most any language to write code that involves both List<Base> and List<Derived> and not have to explicitly cast on occasion.
Brian