Extra bindings in instance declarations

I have often wished that an instance declaration could have extra bindings, i.e., bindings that are not the class methods. Today you would have to make such bindings global. I dislike making bindings have a larger scope than they have to, since that impedes understanding. It would be a bad idea to allow the extra bindings with no indication that they are extra, since that would make a misspelled method name not be an error. So there should be some syntax that indicates what is extra.

Example:

class C a where
  m1 :: a -> a
  m2 :: a -> a

instance C Int where
  m1 x = f x + 1     -- method binding
  m2 x = f x + 2     -- method binding
 where
  f x = x * x        -- extra binding

This is just a suggested syntax, but it seems to make sense.

Is there anyone else who has missed this feature? I’ve implemented in MicroHs. It was trivial.

17 Likes

There are times where I just provide the implementation as a separate function that the end-user can call without going through the indirection of the typeclass constraint. Of course it’s not very practical for classes like Num with lots of operators, for classes with one function it usually provides a nicer interface with we try to keep most domain-related functions monomorphic.

1 Like

Yes, although I don’t like the idea of making type classes even more of a special thing than they already are. I’m considering only ever defining type classes in the future that have a single method <Class>Impl, like this:

class C a where
  cImpl :: CImpl a

data CImpl a = MkCImpl {
  m1Impl :: a -> a,
  m2Impl :: a -> a
}

instance C Int where
  cImpl =
    let f x = x * x
    in MkCImpl {
         m1Impl = f x + 1,
         m2Impl = f x + 1
       }

m1 :: C a => a -> a
m1 = m1Impl cImpl

m2 :: C a => a -> a
m2 = m2Impl cImpl

This is much more uniform and allows us to use any normal Haskell machinery to implement type classes and instances. For example you can do stuff like

fromF :: (a -> a) -> CImpl a
fromF f = MkCImpl { m1Impl = f x + 1, m2Impl = f x + 2 }

instance C Integer where
  cImpl = fromF (\x -> x * x * x)

Yes, although I don’t like the idea of making type classes even more of a special thing than they already are.

There is another way to look at this that makes type classes less of a special thing. A type class conflates two things: it is both a data type (of dictionaries) and a constraint. The problem is that the language hides the “data type” side of type classes, with instance being an ad hoc syntax for defining dictionaries. If type classes were explicitly identified as data types, then a dictionary could be defined as just a value, and we could just use where clauses as usual.

Using single-method classes is one way to identify a type class and a data type. There is also this old GHC proposal for another approach: Allow direct access to underlying concrete class dictionaries by tysonzero · Pull Request #324 · ghc-proposals/ghc-proposals · GitHub

3 Likes

In reply to the original post by @augustss:

Yep, I’ve wanted this on numerous occasions. Would be a nice convenience to have.

1 Like

It doesn’t have to be a special case for instance declarations. We can take a cue from Luca Cardelli’s version of ML from early 80s. Instead if having bindings simply be a list of bindings we could also allow

let
  bindingsL
in
  bindingsG

Which would make bindingsL and bindingsG visible to each other, but only bindingsGwould be visible outside. Similarely, we should have

  bindingsG
where
  bindingsL

Example:

let
  let sqr x = x*x
  in  f x = sqr x + 1
      g x = sqr x - 1
in. ... use f and g ...  -- sqr not visible here

I’ve wished for this feature now and then as well. It would make modules without an export list nicer as well.

module M where. -- only exports f, g
f x = sqr x + 1
g x = sqr x - 1
where
sqr x = x * x

With this my proposed extension to instance would just be a use case.

I frequently miss this for instance declarations, so end up with the helper function being in the module scope which annoys me but am resigned to it.

1 Like

Maybe I’ll make a GHC proposal.

2 Likes

I apologize in advance for being a killjoy.

Given how bloated GHC already is, I think (for everybody’s sanity) the usefulness threshold for proposed features should become higher and higher, especially regarding syntactic extensions. And I don’t think this proposal meets my own subjective threshold.

These extra bindings are a nice convenience, but ultimately they are not substantially different from a toplevel definition that is not exported. Yes, visibility restrictions cannot be more granular than the file level. But that’s a design choice that Haskell made a long time ago, and keeps at least this part of the language simple.

5 Likes

I’m used to having my Haskell proposals rejected, just to show up later on. :grinning_face: (E g., tuple sections, overloaded strings, record wildcards, …)

8 Likes

ML has local decls1 in decls2 end to limit the scope of decls1 to decls2. I think such a construct would be a principled solution to limiting scope. In particular, I’d like to also have local datatypes.

However, the problem with local definitions show up when you want to have checked documentation and tests for them. So, in practice, one simply makes .Internal modules that export everything (for testing and documentation) and then some official modules that export the actual API.

I agree. Small syntax extensions can make things much nice for Haskell experts whilst raising a barrier for non-experts. Ă–mer Sinan AÄźacan has written an article I like about this at osa1 - Some arguments against small syntax extensions in GHC

I would make an exception for syntax extensions that remove irrelevant exceptions, unify disparate concepts, or are otherwise an overall simplification. I haven’t looked at the proposal in this thread closely enough to know whether it satisfies that criterion.

3 Likes

I think extending let binds in expr to also allow let binds in binds is a pretty natural extension. But I will not argue strongly for it.

2 Likes

+1 here; I’d certainly use this.

On a minor note I’d prefer to not have the extra where keyword there as a separator; in particular it reads better to me to have helper functions on the top (again as with let..in). I’m not sure if a separator is needed for technical reasons (I see no such one now though).

instance C Int where
  f x = x * x
  m1 x = f x + 1
  m2 x = f x + 2

(I guess this might prevent the future GHC proposal from breaking the parser.)


Extra shower thought: let’s generalize the where and let in instances and classes:

let m1 :: a -> a
    m2 :: a -> a
 in class C a
let f x = x * x
 in instance C Int where
      m1 x = ...

One thing I’ve thought about in the past is to just allow any normal bindings in the where block of an instance declaration. Then you can bundle your methods like this:

class C a where
  m1 :: a -> a
  m2 :: a -> a

data CImpl a = MkCImpl {
  m1 :: a -> a,
  m2 :: a -> a
}

fromF :: (a -> a) -> CImpl a
fromF f = MkCImpl { m1 = f x + 1, m2 = f x + 2 }

instance C Integer where
  cube x = x * x * x -- cube isn't a method, so it is ignored
  MkCImpl {..} = fromF cube -- defines the m1 and m2 methods

This is a simplification in some sense: it unifies the behaviour of instance ... where ... with module ... where ... and ... = ... where ....

2 Likes

Fun: I suppose you could imagine if Haskell had instead extended data, for defining classes.

data class <signature> = <record-def>
data class C a = C {

  m1 :: a → a,

  …

}

And then instance became an extension of top-level patbind:

instance <head> = <rhss>
instance C Integer = C { m1 = …, … } where cube x = …
1 Like