Is it possible to define a function on only one variant of a type?

This scenario seems to happen to me all the time. In a main function, I do pattern matching (case ... of) to identify what variant I have to deal with, to decide which subfunction to call. This subfunction should then be applied to only that variant. But I can’t find a way to define a function only on a variant. So if I write such a subfunction, unfortunately, I have to define it on the whole type, and match again to extract the variant (despite me and the compiler already knowing which variant it is).
Is there a solution here?

2 Likes

Could you share a snippet to illustrate the problem?
You can pass the values of the constructors to each subfunction:
https://ellie-app.com/fcS2tgmfp5xa1
If you need to ensure the function can only receive the specific variant, you can do this way:
https://ellie-app.com/fcS2sDJZ2Mma1

1 Like

You have to remove the type constructor on the sub function. So if your type is

type Foo = Bar Int String | Baz (List String) Foo

you could do

handleFoo : Foo -> Float
handleFoo foo =
    case foo of
        Bar theInt theString ->
            handleBar theInt theString

        Baz theList theFoo ->
            handleBaz theList theFoo

handleBar : Int -> String -> Float
handleBar theInt theString = ...

handleBaz : List String -> Foo -> Float
handleBaz theList theFoo = ...

(Though I’m usually happy just having a long function if it has a clear separation into cases; I would only bother with factoring out the cases like this if there was a good reason to do it. If I end up with a bunch of functions named handleBar and handleBaz, I don’t really think that’s an improvement. In particular, the update function often works like that.)

1 Like

Thanks for your replies.
Actually my variants “constructors” take records as arguments, like this:

type MyType
    = Variant1 { field1 : List String, field2 : ..., fieldN: ...}
    | Variant2 { field1 : List String, field2: .... FieldN+1: ...}

I mean, there are a lot of fields in common, and also some fields that are differents depending on the variant. It’s a bit heavy to have to repeat all the fields of the record to define the function, but I guess that’s the best solution.

In that case, you could do

type alias Variant1Record = { field1 : List String, … }
type alias Variant2Record = { field1 : List String, ... }

type MyType = Variant1 Variant1Record | Variant2 Variant2Record

handleMyType : MyType -> Int
handleMyType mt = case mt of
    Variant1 rec -> handleVariant1 rec
    Variant2 rec -> handleVariant2 rec

handleVariant1 : Variant1Record -> Int
handleVariant1 rec = …

This way, you only need to list the fields once.

2 Likes

Oh, and you can also look into Elm’s extensible records to maybe factor out the repeated fields.

2 Likes

Also, when there is a good reason for fields to be common to multiple variant, I found that it almost always makes the code clearer to extract those fields in common like so

type alias MyType =
  { common1 : ...
  , common2 : ...
  , specific : Specific
  }

type Specific
  = Variant1 { field1 : ..., field2 : ... }
  | Variant2 { field3 : ... }
3 Likes

See also this post discussing how record and custom types can be modelled using math (its not hard):

Pulling out the common fields is akin to extracting a common factor from some expression. I tend to think of product-sum-product form with all common fields pulled to the outer product as what you are generally aiming for.

This is in product-sum-product form:

-- Outer product
type alias MyType =
  { common1 : ...
  , common2 : ...
  , specific : Specific
  }

-- Sum
type Specific
  = Variant1 { field1 : ..., field2 : ... } -- Inner Product
  | Variant2 { field3 : ... } -- Inner Product
1 Like

You can also write a function that works over some subset of the fields:

myFun : { a | field3 : String, field8 : Int } -> Something

Then any custom type variant with those fields can be passed to it (removing the constructor first of course).

type Specific
  = Variant1 { field1 : ..., field2 : ..., field3: String, ..., field8: Int }
  | Variant2 { field3 : String, ..., field8: Int }
  | VariantX ...

fnSomeVariants  : Specific -> Something
fnSomeVariants s = 
    case s of 
        Variant1 fields -> myFun fields
        Variant2 fields -> myFun fields
        _ -> DefaultSomething
2 Likes

Nice, I didn’t know you could do that.