nim-lang / RFCs

A repository for your Nim proposals.
136 stars 26 forks source link

Allow borrowing all operations on a distinct type #450

Open ajusa opened 2 years ago

ajusa commented 2 years ago

@beef331 recommended I open an RFC for my use case, so here's an attempt at describing what I think would be nice to have. First time writing an RFC, please let me know if something isn't clear!

Problem

type english = distinct string
type german = distinct string
proc checkValid(str: english) = discard
proc checkValid(str: german) = discard
var text = "hello! ".english
text &= "how are you?".english # fails to compile, as &= isn't defined for my distinct type.

So technically, Nim has a way to solve the issue outlined above - just use the borrow pragma. Adding this line fixes the issue:

proc `&=` *(a: var english, b: english) {.borrow.}

This is a bit painful once you start wanting to define more and more procs for your distinct type, which leads to lots of extra code. When writing this RFC, I had to look up the exact type definition and proc for appending to a string, as I forgot the first argument needed to be a var string. Nim further recommends that if you are doing this for a numeric type, just create a set of templates that will borrow most of the relevant procs for you, to avoid code duplication.

A lot of the time I create a distinct type though, I want the existing operations of the base type, but I want the type safety of not being able to use the base type for my procs. Eg, the following example is usually why I want a distinct.

type english = string
proc hello(str: english): string = "hello!"
var a: english
echo a.hello() # works
echo "ich bin".hello() # passing german into a proc that expects english, because the base type of english is string

Keep in mind that is my use case - others use distinct for different reasons, but the proposal here shouldn't affect them.

Why not just use converters? Well adding

converter toEnglish(a: string): english = a.english
converter toString(a: english): string = a.string

doesn't fix the issue.

If you want to borrow all of the operations of the base type of a distinct, then why not use type aliases?

type english = string
type german = string
proc checkValid(str: english) = discard # compiles
proc checkValid(str: german) = discard # doesn't compile, redefinition of 'checkValid'; previous declaration here

Proposal

Extend the borrow program within a type definition to borrow all of the procs of the base type. The example from above would be rewritten as

type english {.borrow: all.} = distinct string
type german {.borrow: all.} = distinct string
proc checkValid(str: english) = discard
proc checkValid(str: german) = discard
var text = "hello! ".english
text &= "how are you?".english # works as all of the string procs would be borrowed

This improves type safety and clarity over using type conversions everywhere (eg converting both arguments to string for the append, then converted both into english). Additionally, this proposal doesn't have any breaking syntax or new syntax - it's still just a pragma.

Nim already has this defined for accessing fields of the base type when using distinct: {.borrow: `.`.} in the type definition. This proposal is just an extension to that pragma, allowing access to all the procs of the base type as well.

metagn commented 2 years ago

Not to diminish the proposal, but generic aliases kind of work in the example case, although this is likely buggy and could be too impractical for most purposes.

type
  Language = enum
    English
    German

  LanguageString[L: static Language] = string

  english = LanguageString[English]
  german = LanguageString[German]

proc checkValid(str: english) = echo "got english"
proc checkValid(str: german) = echo "got german"
var text = "hello! ".english
text &= "how are you?".english
checkValid text
checkValid "hallo".german

Obviously this is just a hack to add a layer of indirection to trick the overloading mechanism and generics might not be applicable to every case. This is just the closest thing I can think of to adding "tags" to types (like string {.tags: [English, GrammarChecked].}) and a simple weak distinct mechanism would be useful enough.

planetis-m commented 2 years ago

Why not type english = string then?

beef331 commented 2 years ago

Why not type english = string then?

proc checkValid(str: english) = discard
proc checkValid(str: german) = discard

Is why not, aliases should not allow the above to happen. They want to easily be able to have a distinct type that allows them to implement custom behaviour without having to manually borrow everything. This means you can use english in place of string but not string in place of english.