modularml / mojo

The Mojo Programming Language
https://docs.modular.com/mojo/manual/
Other
22.97k stars 2.59k forks source link

[stdlib][Discussion] Implement `operator` module #2924

Open laszlokindrat opened 4 months ago

laszlokindrat commented 4 months ago

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 commented 4 months ago

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 commented 4 months ago

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 commented 4 months ago

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 commented 4 months ago

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 commented 4 months ago

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 commented 4 months ago

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 commented 1 week ago

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 commented 1 week ago

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 commented 1 week ago

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.