nim-lang / RFCs

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

Procs Bound Exclusively to Objects #426

Closed Acmion closed 1 year ago

Acmion commented 2 years ago

Procs Bound Exclusively to Objects

Abstract

Some procs should optionally be bound exclusively to objects. In practice this would mean that certain procs would only be accessible via object dot notation, rather than accessible directly from the current module.

This could make type imports more convenient and more like how other popular languages do it. This would also clean up the list of current definitions within the current module.

Motivation

Description

There should be an option to bind procs directly to a specific type and not just leave them as "unbinded" procs. Currently procs operating on objects will always populate the definitions of the current module, even if this is not always necessary nor wanted. As such, intellisense will start suggesting a lot of stuff, even if some of it may be completely impractical (for example, objects that have complicated "constructors"). Not importing these procs is not an option, because otherwise dot notation will not work at all.

Object fields can already not be accessed without the instance and dot notation.

These procs could also be callable via the type itself and would thus act akin to static methods in C#.

Alternatively, this could be added as an option to the import command, where from module import Book.* (note the .*) would import all related procs to Book, but only via dot notation. This would remove the need for cases such as: from module import Book, Book_proc_0, Book_proc_1, ..., Book_proc_n

Implementation alternatives:

Downsides:

Examples

Before

books.nim:

type Book* = ref object
    year_of_publication*: int
    author*: string
    name*: string

proc was_released_before_or_during_1970*(this: Book): bool= 
    return this.year_of_publication >= 1970

proc calculate_years_since_1970*(this: Book): int = 
    if this.was_released_before_or_during_1970():
        return this.year_of_publication - 1970

    return -1

example.nim:

import books

# works
var book = Book(year_of_publication: 1982, author: "John Doe", name: "Awesome Book")

# works
echo book.name

# works
echo book.calculate_years_since_1970

# works
echo book.calculate_years_since_1970()

# does not work, because it should not
echo name(book)

# works, but it is not as clear that this method actually belongs to book and also a slight contradiction to why name(book) does not work
echo calculate_years_since_1970(book)

example_alt.nim:

from books import Book

# works
var book = Book(year_of_publication: 1982, author: "John Doe", name: "Awesome Book")

# works
echo book.name

# does not work, even if this usually works in other programming languages
echo book.calculate_years_since_1970

# does not work, even if this usually works in other programming languages
echo book.calculate_years_since_1970()

# does not work, because it should not
echo name(book)

# does not work
echo calculate_years_since_1970(book)

After

Note: changes marked with #####

books.nim:

type Book* = ref object
    year_of_publication*: int
    author*: string
    name*: string

    ##### notice the indentation for the procs
    proc was_released_before_or_during_1970*(this: Book): bool= 
        return this.year_of_publication >= 1970

    ##### notice the indentation for the procs
    proc calculate_years_since_1970*(this: Book): int = 
        if this.was_released_before_or_during_1970():
            return this.year_of_publication - 1970

        return -1

example.nim:

import books

# works
var book = Book(year_of_publication: 1982, author: "John Doe", name: "Awesome Book")

# works
echo book.name

# works
echo book.calculate_years_since_1970

# works
echo book.calculate_years_since_1970()

# does not work, because it should not
echo name(book)

##### does not work, because it should not
echo calculate_years_since_1970(book)

##### works, somewhat like static methods in C#
echo Book.calculate_years_since_1970(book)

example_alt.nim:

##### note the .*, which is an alternative for an augmented import
from books import Book.*

# works
var book = Book(year_of_publication: 1982, author: "John Doe", name: "Awesome Book")

# works
echo book.name

##### works
echo book.calculate_years_since_1970

##### works
echo book.calculate_years_since_1970()

# does not work, because it should not
echo name(book)

# does not work
echo calculate_years_since_1970(book)

Backward incompatibility

This would depend on the implementation.

Other

A discussion regarding the problems with code completion when using unbinded functions in Julia.

Even if this would not be implemented in its entirety, I believe that the "augmented import command" could be worth considering.

n0bra1n3r commented 2 years ago

This seems related to @Araq's more general RFC on type-bound procs which I'm looking forward to: https://github.com/nim-lang/RFCs/issues/380.

Acmion commented 2 years ago

@n0bra1n3r Yeah, good catch!

As the issue states, operators are even more problematic than standard procs.

demotomohiro commented 2 years ago

Just copying what other major programming language do to Nim doesn't seems good idea unless there is a good reason.

All those who have experience with other programming languages and are learning Nim could benefit from this.

For people who is not familiar with these feature, that just makes learning Nim harder. Also adding new features makes language spec complicated and maintaining Nim compiler harder.

Currently procs operating on objects will always populate the definitions of the current module, even if this is not always necessary nor wanted. As such, intellisense will start suggesting a lot of stuff, even if some of it may be completely impractical (for example, objects that have complicated "constructors"). Not importing these procs is not an option, because otherwise dot notation will not work at all.

If a module has so many procs and you want to import a few of them because importing them all cause problem, I think you should split the module to smaller multiple modules. Even if you can bind procs to a specific type, from module import Book or from module import Book.* can import many unused procs if the imported module defines many procs that are bound to Book object type and you use only a few of them, isn't it?

Having less definitions makes reasoning about code easier. Having less definitions makes certain bugs easier to detect (especially since overloading is possible)

Can your RFC reduce a number of procs need to be implemented? Isn't it just make easier to reduce a number of imported procs?

Some procs have quite general names and can thus be confused with other procs within the current module.

tables module defines multiple types (Table, OrderedTable, CountTable) and it defines multiple general name procs (add, len, del, etc). There are also add, len and del procs for seq type. Can these same name procs be confusing? I can use these procs without problems:

import tables

var t = {'a': 5, 'b': 9, 'c': 13}.toTable
t.del 'a'
echo t.len

var s = @[0, 1]
s.del 0
echo s.len

var ct: CountTable[char]
ct.inc 'a'
ct.inc 'b'
ct.del 'a'
echo ct.len

Even if there are same name and same parameter type procs in different module, you can disambiguate a call by adding module name like modulename.procname(). (But I don't think having mutiple procs with same name and same parameter types in different modules are good idea.)

After you wrote following code, when you write a., your intellisense should suggests only procs that has Table type as first paramter.

import tables

var
  a = {1: "one", 2: "two"}.toTable

Anyway, If books.nim defines proc was_released_before_or_during_1970*(this: Book): bool = and it is bound to Book ref object type and example.nim also defines same name proc and need to import Book type, your RFC still doesn't seems to solve the problems.

Araq commented 1 year ago

More or less a duplicate of https://github.com/nim-lang/RFCs/issues/380