Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[stdlib][Discussion] Implement operator module #2924

Open
laszlokindrat opened this issue Jun 2, 2024 · 9 comments
Open

[stdlib][Discussion] Implement operator module #2924

laszlokindrat opened this issue Jun 2, 2024 · 9 comments

Comments

@laszlokindrat
Copy link
Contributor

laszlokindrat commented Jun 2, 2024

As part of a larger effort to clean up and open source the math module, we have removed a number of functions that would mirror arithmetic operators. In most cases, the value of these functions is limited, since the operators can (and in fact should) be used directly, e.g.

var a = b + c # preferred, more pythonic
var a = add(b, c)

However, for code that passes around these as function pointers or parameters, it might be convenient to import these from a central module rather than having to define wrappers for each of these. Python's operator exists for slightly different reasons, but IMO it sets a pretty clear precedent and has a clear scope. The main difference I envision compared to Python is that in Mojo we probably want a trait for each of these methods. Consequently, this could be a natural place for Absable (and others) to live in, instead of being a builtin and implicitly imported everywhere. This is another reason why I'm considering work on this module: declaring these traits in a single place would make it easier to compose them, and build more complex abstraction on top of them.

Are there any concerns with this? I am also looking for ideas on how to test these: the tests should not rely on particular implementations of these methods (e.g. those of Int's).

To be clear, this is not very high priority for the stdlib team, but since it's a self-contained, relatively simple, and fairly well defined module, it would be something that the community could work on, if we decide to move forward with it. In particular, seems like this might include many "good first issue" type tasks.

@soraros
Copy link
Contributor

soraros commented Jun 2, 2024

Idea: If we were to introduce traits to each operator, could we name them Has** instead of **able?

I'm not suggesting that we change what we have, which are perfectly good (like Hashable, Copyable, etc.). Rather, I propose that we could introduce "syntactic traits" which, by itself, does not have any predefined semantics, such as having a __add__ method. We shouldn't even add these trait to the conforming types. This is very different from a numerical trait like Additive, which also requires a __add__ method.

@gabrieldemarmiesse
Copy link
Contributor

In python, some functions are only available in the operator module. For example operator.index(). In Mojo, index() is builtin and imported everywhere, but it's not very practical, as index is a common name for a variable/argument. Putting it in an operator module would help clean that up.

@martinvuyk
Copy link
Contributor

martinvuyk commented Jun 4, 2024

Has** etc instead of **able

idea: Can**

And somehow make this even shorter?
for example for types that are used often

trait CanCollect(CanCopy, CanMove, CanDelete):
  ...

feels more natural

fn some_func[T: CanMove | CanCopy](somthing: T):
  @parameter
  if T.can[CanCopy]():
    ...
  else:
    ...

@laszlokindrat
Copy link
Contributor Author

In python, some functions are only available in the operator module. For example operator.index(). In Mojo, index() is builtin and imported everywhere, but it's not very practical, as index is a common name for a variable/argument. Putting it in an operator module would help clean that up.

+1

Idea: If we were to introduce traits to each operator, could we name them Has** instead of **able?

I'm not suggesting that we change what we have, which are perfectly good (like Hashable, Copyable, etc.). Rather, I propose that we could introduce "syntactic traits" which, by itself, does not have any predefined semantics, such as having a __add__ method. We shouldn't even add these trait to the conforming types. This is very different from a numerical trait like Additive, which also requires a __add__ method.

Also +1, I like the expression "syntactic traits" for this. I think we would use these to build common compositions that some types would actually conform to explicitly (t.g. EqualityComparable), but explicit conformance to just HasLessThan will be rare. For this reason I also suggest that we call it Has* instead of Can*, since these traits really only just promise the existence of a method.

@laszlokindrat
Copy link
Contributor Author

Regarding testing, what if we have things like this:

struct MockLessThan(HasLessThan):
    fn __lt__(self, other: Self) -> Bool:
        return True

def test_less_than():
     var m = MockLessThan()
     var r = m.__lt__(m)
     assert_true(_type_is_eq[__type_of(r), Bool]())
     assert_true(r)

I'm looking for minimal ways to "glue" these traits and their methods in the module, so that we don't accidentally move/remove them.

@martinvuyk
Copy link
Contributor

I think we would use these to build common compositions that some types would actually conform to explicitly

I sitll think Can** will be shorter for composite traits e.g. StringableCollectionElement becomes CanCollect & HasStr, because Has** could be reserved for base traits ("syntactic traits"), and concatenating is shorter as well CanCompareCollect.

I like the idea of having these base traits a lot because once we are able to do T: HasCopy & HasGreaterThan we will unlock true interface conformance regardless of types or inheritance or composition.

@nmsmith
Copy link
Contributor

nmsmith commented Sep 21, 2024

It might make sense to just use the dunder method name as the trait name. So there could be an __int__ trait and a __len__ trait etc.

@martinvuyk
Copy link
Contributor

Hmmm... although it's not explicitly stated in the style guide, to my understanding, the convention is camel case for types and traits.

Also, say for example that Formattable (a type having a fn format_to(self, inout writer: Formatter) method) were to be renamed as format, that would conflict with the function name or potentially variable names. And a third aspect would be that underscore is normally used to denote that something is for private use. And a fourth albeit very subjective opinion, is that I think HasInt, HasLen, HasAdd, HasSub, HasEq, HasGe read a bit more naturally (I would even dare say more Pythonic (?) ).

@nmsmith
Copy link
Contributor

nmsmith commented Sep 22, 2024

I'm not sure why you're talking about renaming Formattable to a lowercase name. All I'm suggesting is that it might be worth using special names for the traits that correspond to dunder methods—just as dunder methods themselves have special names.

underscore is normally used to denote that something is for private use

That's what a single leading underscore means. __<identifier>__ doesn't inherit that meaning. There are some non-magic-method identifiers using that naming scheme that are meant to be public, e.g. __file__.

I'm not set on any particular naming scheme. I'm just sharing some ideas.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants