How to add LanguagePrimitives.GenericZero / get_Zero to System.String?
Asked Answered
B

1

9

Note: I added a lot of Of interest comments at the end. These are not mean to suggest that one should use inline and static type parameters willy nilly, they are there so that one does not have to spend hours searching lots of SO questions related to this question to better understand these concepts.

I know that when one needs to make a function generic and needs a zero (0) value F# provides GenericZero.

Resolves to the zero value for any primitive numeric type or any type with a static member called Zero.

So this leads me to believe that to use GenericZero with a string type I only have to add a static member called Zero.

Since System.String is part of the .Net framework, modifying the .Net source code is not what should be done. However F# provides Type Extensions.

Type extensions let you add new members to a previously defined object type.

Also F# provides the String module, but that lacks GenericZero.

For a good tutorial on creating type extensions reference: Attaching functions to types.

The code I tested with:

This is in a project called Library1

namespace Extension.Test

module Extensions = 

    type System.String with
        static member Something = "a"

        static member StaticProp
            with get() = "b"

        static member Zero
            with get() = "c"

This is in a project called Workspace

namespace Extension.Test
module main =

    open Extensions

    [<EntryPoint>]
    let main argv = 

        let stringSomething = System.String.Something
        printfn "something: %s" stringSomething

        let staticProperty = System.String.StaticProp
        printfn "staticProperty: %s" staticProperty

        let zeroProperty = System.String.Zero
        printfn "zeroProperty: %s" zeroProperty

        let inline addTest (x : ^a) (y : ^a) : ^a =
            x + y

        let intAdd = addTest 2 LanguagePrimitives.GenericZero
        let floatAdd = addTest 2.0 LanguagePrimitives.GenericZero
//        let stringAdd = addTest "a" LanguagePrimitives.GenericZero

        printfn "intAdd: %A" intAdd
        printfn "floatAdd: %A" floatAdd
//        printfn "stringAdd: %A" stringAdd

        printf "Press any key to exit: "
        System.Console.ReadKey() |> ignore
        printfn ""

        0 // return an integer exit code

when run outputs:

something: a
staticProperty: b
zeroProperty: c
intAdd: 2
floatAdd: 2.0
Press any key to exit

So I am creating and accessing extension members and using GenericZero without any problems.

The last part is to use GenericZero for a string, however uncommenting the line

let stringAdd = addTest "a" LanguagePrimitives.GenericZero

results in the error:

The type 'string' does not support the operator 'get_Zero'

I did check the F# spec but found nothing of help.

Can I add GenericZero to type System.String, did I do something wrong in the code, or did I miss something in the documentation?

TL;DR

SO questions of interest when searching

F# - How do I extend a type with get_Zero so I can use an existing type generically?
which IMO is a misleading title that should be changed after reading the answers.

What/where is get_Zero in F#'s int?
which has a nice comment from Jack:

Zero doesn't make sense on string if you think of it in terms of numerics; however, it does make sense to have a Zero member on string which returns the empty string, as that would make string a monoid under string concatenation.

F# documentation of interest

Automatic Generalization

The F# compiler, when it performs type inference on a function, determines whether a given parameter can be generic. The compiler examines each parameter and determines whether the function has a dependency on the specific type of that parameter. If it does not, the type is inferred to be generic.

Type Inference

The idea of type inference is that you do not have to specify the types of F# constructs except when the compiler cannot conclusively deduce the type.

For those types that you do not specify explicitly, the compiler infers the type based on the context. If the type is not otherwise specified, it is inferred to be generic.

Generics

F# function values, methods, properties, and aggregate types such as classes, records, and discriminated unions can be generic. Generic constructs contain at least one type parameter, which is usually supplied by the user of the generic construct. Generic functions and types enable you to write code that works with a variety of types without repeating the code for each type. Making your code generic can be simple in F#, because often your code is implicitly inferred to be generic by the compiler's type inference and automatic generalization mechanisms.

Statically Resolved Type Parameters

A statically resolved type parameter is a type parameter that is replaced with an actual type at compile time instead of at run time. They are preceded by a caret (^) symbol.

Statically resolved type parameters are primarily useful in conjunction with member constraints, which are constraints that allow you to specify that a type argument must have a particular member or members in order to be used. There is no way to create this kind of constraint by using a regular generic type parameter.

In the F# language, there are two distinct kinds of type parameters. The first kind is the standard generic type parameter. These are indicated by an apostrophe ('), as in 'T and 'U. They are equivalent to generic type parameters in other .NET Framework languages. The other kind is statically resolved and is indicated by a caret symbol, as in ^T and ^U.

Constraints

Inline Functions

When you use static type parameters, any functions that are parameterized by type parameters must be inline.

EDIT

Here is an example that uses GenericZero for a user defined type without using an extension that works and two variations showing that GenericZero does NOT work for intrinsic extension and optional extension

Run the program first to see GenericZero work then uncomment the lines in Program.fs to see the errors for intrinsic extension and optional extension.

An intrinsic extension is an extension that appears in the same namespace or module, in the same source file, and in the same assembly (DLL or executable file) as the type being extended.

An optional extension is an extension that appears outside the original module, namespace, or assembly of the type being extended. Intrinsic extensions appear on the type when the type is examined by reflection, but optional extensions do not. Optional extensions must be in modules, and they are only in scope when the module that contains the extension is open.

In Library1.fs in project Library1

namespace Extension.Test

module module001 = 

    // No extension
    type MyType01(x: string) =
        member this.x = x

        override this.ToString() = this.x.ToString()

        static member Something = MyType01("a")        
        static member (+) (mt1 : MyType01, mt2 : MyType01) = MyType01(mt1.x + mt2.x)
        static member (+) (mt1 : MyType01, s : string) = MyType01(mt1.x + s)
        static member (+) (s : string, mt2 : MyType01) = MyType01(s + mt2.x)

        static member Zero
            with get() = MyType01("b")

    // uses intrinsic extension
    type MyType02(x: string) =
        member this.x = x

        override this.ToString() = this.x.ToString()

        static member Something = MyType02("g")        
        static member (+) (mt1 : MyType02, mt2 : MyType02) = MyType02(mt1.x + mt2.x)
        static member (+) (mt1 : MyType02, s : string) = MyType02(mt1.x + s)
        static member (+) (s : string, mt2 : MyType02) = MyType02(s + mt2.x)

//        static member Zero
//            with get() = MyType02("h")

    // uses optional extension
    type MyType03(x: string) =
        member this.x = x

        override this.ToString() = this.x.ToString()

        static member Something = MyType03("m")        
        static member (+) (mt1 : MyType03, mt2 : MyType03) = MyType03(mt1.x + mt2.x)
        static member (+) (mt1 : MyType03, s : string) = MyType03(mt1.x + s)
        static member (+) (s : string, mt2 : MyType03) = MyType03(s + mt2.x)

//        static member Zero
//            with get() = MyType03("n")


module module002 = 

    open module001

    // intrinsic extension
    type MyType02 with

        static member Zero
            with get() = MyType02("h")

in Library2.fs in project Library2

namespace Extension.Test

open module001

module module003 = 

    type MyType01 with 

        static member Anything = MyType02("c")

    type MyType02 with 

        static member Anything = MyType02("i")

    // optional extension
    type MyType03 with 

        static member Anything = MyType03("p")

        static member Zero
            with get() = MyType03("n")

in Program.fs in project Workspace

namespace Workspace

open Extension.Test.module001
open Extension.Test.module002
open Extension.Test.module003

module main =

    [<EntryPoint>]
    let main argv = 


        let staticFromBaseType = MyType01.Something
        printfn "MyType01 staticFromBaseType: %A" staticFromBaseType

        let staticFromExtensionType = MyType01.Anything
        printfn "MyType01 staticFromExtensionType: %A" staticFromExtensionType

        let zeroValue = MyType01.Zero
        printfn "MyType01 zeroValue: %A" zeroValue

        let (genericZero: MyType01) = LanguagePrimitives.GenericZero
        printfn "MyType01 genericZero: %A" genericZero

        let staticFromBaseType = MyType02.Something
        printfn "MyType02 staticFromBaseType: %A" staticFromBaseType

        let staticFromExtensionType = MyType02.Anything
        printfn "MyType02 staticFromExtensionType: %A" staticFromExtensionType

        let zeroValue = MyType02.Zero
        printfn "MyType02 zeroValue: %A" zeroValue

//        let (genericZero: MyType02) = LanguagePrimitives.GenericZero
//        printfn "MyType02 genericZero: %A" genericZero


        let staticFromBaseType = MyType03.Something
        printfn "MyType03 staticFromBaseType: %A" staticFromBaseType

        let staticFromExtensionType = MyType03.Anything
        printfn "MyType03 staticFromExtensionType: %A" staticFromExtensionType

        let zeroValue = MyType03.Zero
        printfn "MyType03 zeroValue: %A" zeroValue

//        let (genericZero: MyType03) = LanguagePrimitives.GenericZero
//        printfn "MyType03 genericZero: %A" genericZero

        let inline addTest (x : ^a) (y : ^a) : ^a =
            x + y

        let intAdd = addTest 2 LanguagePrimitives.GenericZero
        let floatAdd = addTest 2.0 LanguagePrimitives.GenericZero
        let (myType01Add : MyType01) = addTest (MyType01("d")) LanguagePrimitives.GenericZero
//        let (myType02Add : MyType02) = addTest (MyType02("d")) LanguagePrimitives.GenericZero
//        let (myType03Add : MyType03) = addTest (MyType03("o")) LanguagePrimitives.GenericZero

        printfn "intAdd:      %A" intAdd
        printfn "floatAdd:    %A" floatAdd
        printfn "myType01Add: %A" myType01Add
//        printfn "myType02Add: %A" myType02Add
//        printfn "myType03Add: %A" myType03Add


        printf "Press any key to exit: "
        System.Console.ReadKey() |> ignore
        printfn ""

        0 // return an integer exit code
Bioecology answered 9/5, 2016 at 13:30 Comment(34)
Of interest: Could not extend operators in F#?Bioecology
Of interest: How to define a type extension for T[] in F#?Bioecology
Of interest: How can I define an extension member on an F# unit of measure?Bioecology
Of interest: F# Extention Methods on Lists, IEnumberable, etcBioecology
Of interest: F#: Member constraints to help create seemingly dynamic typesBioecology
Of interest: Generic zero for generic functionBioecology
Of interest: Idiomatic way in F# to establish adherence to an interface on type rather than instance levelBioecology
Of interest: F# assuming int when actually dealing with int64Bioecology
Of interest: F# Generic Math: how to write function with op_GreaterThanBioecology
Of interest: F#: Casting to generic typeBioecology
Of interest: What is the difference between the generic signifier ' and the symbol ^ In F# method signaturesBioecology
Of interest: How to write a function for generic numbers?Bioecology
Of interest: Statically Resolved Type ParametersBioecology
Of interest: Can a function of type (unit -> unit) have statically resolved type parameters in F#?Bioecology
Of interest: How to constrain one type parameter by anotherBioecology
Of interest: In depth description of statically resolved type parameters Read the comments.Bioecology
Of interest: F# cryptic generic constraint error messageBioecology
Of interest: How this type annotation works, and why the other one does not?Bioecology
Of interest: Why are functions bound to the first type they are passedBioecology
Of interest: Generically Constraining F# – Part I , Part II, Part IIIBioecology
Of interest: seq<obj> versus seq<float> in F#Bioecology
Of interest: F#: Overloading functionsBioecology
Of interest: How to check if a function's type parameters are statically resolved?Bioecology
Of interest: Type inference failing on generic type with static member constraintBioecology
Of interest: F# explicit member constraints: The type variable ^T could not be generalized because it would escape its scopeBioecology
Of interest: F# A type parameter is missing a constraintBioecology
Of interest: F# missing type constraintBioecology
Of interest: how are abs, sign etc implemented in F#Bioecology
Of interest: Use of inline in F#Bioecology
Of interest: In F#, is it possible to have a tryParse function that infers the target typeBioecology
Of interest: Generic type constraints and duck typingBioecology
Of interest: F#: arithmetic operator and loss of polymorphism (value restriction?)Bioecology
Of interest: F# generics / function overloading syntaxBioecology
This was one of the most thoroughly-prepared questions I've ever seen on Stack Overflow, so +1 for that alone.Bureaucratic
W
5

Extension members aren't considered as part of member constraint resolution, so you're out of luck. With constraints that involve more than one type (such as the constraint on (+)), you can work around this via the use of a second type, but for the constraint on GenericZero there's no good workaround.

Woodsum answered 9/5, 2016 at 13:52 Comment(3)
Ugh! Thanks. Is this documented anywhere?Bioecology
I don't know that it's explicitly covered by the official documentation. But see e.g. #3681642 for confirmation (also #25346746). Also, see fslang.uservoice.com/forums/245727-f-language/suggestions/… for a related user voice item.Woodsum
And see visualfsharp.codeplex.com/workitem/2 for some commentary on why it is nontrivial to add this to the compiler.Woodsum

© 2022 - 2024 — McMap. All rights reserved.