Skip to content

[SUGGESTION] Multiple suffixes for literals, a revisit of Uniform Function Call Syntax #284

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

Closed
msadeqhe opened this issue Mar 16, 2023 · 40 comments

Comments

@msadeqhe
Copy link

msadeqhe commented Mar 16, 2023

In our life, every quantity can have multiple suffixes. For example:

  • 1 million dollar
  • 1 million euro
  • 20% of a bottle of water
  • 5,000 grams of apples
  • 1 hour of worker

In C++1, optional ' may be inserted between the digits of integer and floating-point literals as a separator. Therefore I propose that every suffix optionally may start with ' and mandatory ' must be inserted between them as a separator for multiple suffixes. Now, let's write the code for the above cases:

1'million'dollar
1'million'euro
20'percent'bottle'water
5'000'gram'apple
1'h'worker

Also built-in suffixes optionaly may start with '. They can be uppercase or lowercase. The built-in integer suffixes are L, LL, Z, U, UL, ULL and UZ (if U is provided, it must be the first in C++2, but it can be the last in C++1). The built-in floating-point suffixes are F, L, F16, F32, F64, F128 and BF16 as defined in C++23.

1'ULL
5'000.0'F

Encoding prefixes of character literals and string literals have to be changed to be suffix (similar to C# language), the first suffix have to be without ', because the end of character literals and string literals already have a quote:

'c'u8'symbol
"UTF-8 string view"u8'sv
R"(raw string)"u8's

As an extra feature, boolean literals may have suffixes, but it's really a special case for libraries:

false'news

In addition, we can write user defined suffixes after built-in suffixes:

100'UL'min
1.5'F'ms

Now, the question is what does it have common to uniform function call syntax. To answer this question, the following example is useful:

// In C++1
apple(gram(5'000))

// In C++2 with Uniform Function Call Syntax
// The above expression can be written as the following expression
5'000.gram().apple()

// In my suggestion, we can go further
5'000'gram'apple

So, definitely suffixes are the same as member functions or free functions. We don't need an operator"" to create user defined literals, each member function with the right signature can be used as suffix. Also free functions with the right signature can be used as suffix. For example:

Gram :type = {
    ...

    apple :(this) ->Apple = {
        ...
    }

    ...
}

gram :(value :int) ->Gram = {
    ...
}

x := 5'000'gram'apple;

No, suffixes should never get any extra parameter, it will ruin its purpose of being less verbose (being without parenthesis), and they should not modify this parameter.

Will your feature suggestion improve performance or expressiveness?

Yes, because user defined literals can improve expressivity of the programmer:

// store.sell(orange(box(2)));
a := store.sell(2'box'orange);

// price(water(bottle(2)) + juice(glass(0.5)))
b := price(2'bottle'water + 0.5'glass'juice);

Maybe it improves performance too, because for built-in suffixes the true type of the literal will be sent to the member function call:

hour :(value :float) ->Hour = {
	...
}

x := 1.0'F'hour;

If we rewrite the above example with operator"" in C++1, then the type of value parameter must be long double instead of float, because operator"" only accepts a parameter of type unsigned long long int or long double.

Uniform function call syntax will also improve generic programming for suffixes.

Will your feature suggestion eliminate X% of security vulnerabilities of a given kind in current C++ code?

No.

Will your feature suggestion automate or eliminate X% of current C++ guidance literature?

Yes. My feature suggestion will remove unnecessary extra concept operator"", and it will integrate its usage with normal member functions or free functions. Also It improves code readability.

The purpose of suffix notation is for units or quantities, member function notation is for object-style behaviors or states, and free function notation is for computing.

Describe why you've considered this feature request.

Currently I'm trying to suggest a new string literal for C++2. This suggestion will affect my suggestion about string literals. I hope to read your feedbacks here, therefore I can improve it or I should give up.

@msadeqhe msadeqhe changed the title [SUGGESTION] [SUGGESTION] Multiple suffixes for literals, a revisit of Uniform Function Call Syntax Mar 16, 2023
@msadeqhe
Copy link
Author

msadeqhe commented Mar 17, 2023

I've to correct some parts of my suggestion.

We don't need an operator"" to create user defined literals, each member function with the right signature can be used as suffix. Also free functions with the right signature can be used as suffix.

If using free functions for defining user defined literals seems too much radical change (if we consider binary compatibility with C++1), we should keep using operator"" (or in this suggestion should be renamed to operator'), and give up about making it a part of uniform function call syntax, but user defined suffixes don't need to begin with _ (as restricted in C++1), and their parameter can use any built-in type in addition to unsigned long long int and long double in C++1:

operator 'gram :(value :int) = {
// or simply the above line can be defined like this without operator keyword:
// 'gram :(value :int) = {
    ...
}

x := 5'000'gram;

The goal of my suggestion is about to allow multiple suffixes on literals (for example 1.0'F'ms or 1'box'orange), and the prefix of string literals and character literals should be changed to be suffix, in other words, u8"text"s should be changed to "text"u8's.

@AbhinavK00
Copy link

Writing ' to use literal suffix seems extra work, what if we just define

operator gram :(value :int)

Why not , instead of '?

@SebastianTroy
Copy link

SebastianTroy commented Mar 17, 2023 via email

@msadeqhe
Copy link
Author

msadeqhe commented Mar 17, 2023

Why not , instead of '?

Because , is already a separator for function parameters and lists:

player.buy(2'box'biscuit, 3'000'banana);

@msadeqhe
Copy link
Author

msadeqhe commented Mar 17, 2023

This might be out there, but with unified function call syntax, could we treat literals and built in types the same as user types and call 1.gram() which would be equivalent to gram(1)? Then we don't need to introduce custom operators or new syntax. It would naturally allow chaining too.

Yes, you're right. Literals, member functions and free functions do really the same thing, but in a different style (syntax), and they help programmers to convey more expressively the concept of code.

@jcanizales
Copy link

jcanizales commented Mar 20, 2023

Will your feature suggestion eliminate X% of security vulnerabilities of a given kind in current C++ code?
No.

Might! Anything that eases the use of units, indirectly allow the compiler to catch bugs that we all face every so often. Sure, those bugs of ours aren't as glamorous as the space probe that missed Mars because someone wrote code in non-international units. But they might lead to security vulnerabilities.

@hsutter
Copy link
Owner

hsutter commented Mar 22, 2023

Thanks @msadeqhe for the suggestion, and thanks [edit: @jcanizales @SebastianTroy !!] for the UFCS idea.

Well, this turned into a fascinating little detour, and I think I like the result!

[edit: @jcanizales @SebastianTroy !!]'s intriguing point about UFCS worked today already for string literals... for example, the following already worked:

Gram: type = {
    value:     int;
    operator=: (out this, val: int) = { value = val; }
    print:     (this) = std::cout << "(value)$ grams\n";
}

gram: (val: std::string_view) -> Gram
    = :Gram = sv_to_int(val).value; // (*)

main: () = {
    test := "2".gram();
    test.print();   // prints "2 grams"
}

But it didn't already work for integers because "1." and "1.f" were valid floating point literals, so that . couldn't be parsed as starting a UFCS function call.

I say "were" and "couldn't" because I just changed that... I've long disliked that the C floating point literal syntax allowed omitting the 0 (I find omitting it slightly less readable, though admittedly I've taken advantage of it sometimes just because it was conventional and because, well, laziness).

I've now pushed the above commit that:

  • requires that floating point literals have at least one digit after the ., so that .0 be written explicitly, which I think is a small improvement in its own right; and
  • therefore unlocks general UFCS on numeric literals, which can be useful generally but also for the purpose of this thread.

With the above commit, the following now works too:

Gram: type = {
    value:     int;
    operator=: (out this, val: int) = { value = val; }
    print:     (this) = std::cout << "(value)$ grams\n";
}

gram: (val: int) -> Gram
    = :Gram = val;

main: () = {
    test := 2.gram();
    test.print();   // prints "2 grams"
}

Note this is also safer, because you know right away you parsed a valid... you don't have to do run-time error checking as with the first string_view example in case someone passes a string that doesn't actually contain an integer (or only an integer).

I think this is an interesting path to explore... let me know what you think as you play around with using this style for unit-type literals.


(*) see sample implementations at https://stackoverflow.com/questions/56634507/safely-convert-stdstring-view-to-int-like-stoi-or-atoi

@msadeqhe
Copy link
Author

msadeqhe commented Mar 22, 2023

Thanks @hsutter. I have a question, will C++2 support User-defined Literals? I've tried to overload operator "" in C++2, but it doesn't generate any C++1 code:

operator ""_w: (v: ulonglong) -> ulonglong = {
    return 0;
}

Does it mean we should use UFCS instead of User-defined Suffixes?

@AbhinavK00
Copy link

AbhinavK00 commented Mar 22, 2023

It's probably a probably with ulonglong, try using u64 instead.

Edit: Still doesn't do anything, so its not about ulonglong. Still, cpp2 has no type named ulonglong.

Edit 2: I was wrong

@msadeqhe
Copy link
Author

msadeqhe commented Mar 22, 2023

Yes, u64 doesn't work either. ulonglong is defined in cpp2util.h with the following line:

using ulonglong = unsigned long long;

@hsutter
Copy link
Owner

hsutter commented Mar 22, 2023

will C++2 support User-defined Literals?

For authoring UDLs: I hadn't decided whether or not to support authoring them, and this thread convinces me we should try using UFCS for that first. It's desirable to solve the problem using a more general language feature, if it works well also for this use case, rather than adding a special-purpose language feature.

For consuming existing Cpp1 UDLs: I haven't made any special effort to either support or prevent consuming them, and I generally want things like that to work for compatibility so that Cpp2 can consume existing Cpp1 libraries with high fidelity. I did a quick test using the cppreference UDL page's Coliru example code for UDLs, and it works with main changed to Cpp2:

main: () = {
    x_rad: double = 90.0_deg_to_rad;
    std::cout << std::fixed << x_rad << '\n';

    y: mytype = 123_mytype;
    std::cout << y.m << '\n';

    0x123ABC_print;
    std::cout << "abc"_x2 << '\n';
}

@AbhinavK00
Copy link

It's desirable to solve the problem using a more general language feature, if it works well also for this use case, rather than adding a special-purpose language feature.

While I agree with that, if we already have literals in the langauge, why not just go ahead and let users define theirs?
Cpp2 is already going to consume literals defined in standard llibrary so it is only natural for cpp2 to support UDLs.
it's not a new feature, literals already exist and it's just operator overloading.

@filipsajdak
Copy link
Contributor

@AbhinavK00 you can still define it on the cpp1 side and use it in cpp2. In my project, I have been playing with this interoperability between cpp2 and cpp1 (it is a feature of cpp2). When there will be enough evidence for any solution it can be introduced into the cpp2.

The best way to support that is to write code that uses it (or port some code to cpp2) and show that it works well or badly.

@jcanizales
Copy link

Just wanted to point out that the idea of using UFCS for this was @SebastianTroy 's, not mine 🙂

@hsutter
Copy link
Owner

hsutter commented Mar 22, 2023

@jcanizales @SebastianTroy Eek, thanks -- corrected, and thanks again for the idea and discussion.

@SebastianTroy
Copy link

SebastianTroy commented Mar 22, 2023 via email

@JohelEGP
Copy link
Contributor

JohelEGP commented Mar 29, 2023

Moved to #307.

@JohelEGP
Copy link
Contributor

With the above commit, the following now works too:

Gram: type = {
    value:     int;
    operator=: (out this, val: int) = { value = val; }
    print:     (this) = std::cout << "(value)$ grams\n";
}

gram: (val: int) -> Gram
    = :Gram = val;

main: () = {
    test := 2.gram();
    test.print();   // prints "2 grams"
}

Maybe this works by chance, but you can use Gram directly, without the gram function: https://cpp2.godbolt.org/z/3qax9WfeM.

@msadeqhe
Copy link
Author

msadeqhe commented May 13, 2023

That's interesting. I think it's a bug. AFAIK a type is not supposed to be called like a member function with UFCS, and there isn't a C++ style cast in Cpp2:

x0: = Type(2); // It's a Cpp1-style cast and object construction.
x1: = 2.Type(); // It's not UFCS.

@JohelEGP
Copy link
Contributor

and there isn't a C++ style cast in Cpp2:

x0: = Type(2); // It's a Cpp1-style cast and object construction.

I don't think Cpp2 intends to ban A(B), where A could name a function, type, or variable.

@msadeqhe
Copy link
Author

I don't think Cpp2 intends to ban A(B), where A could name a function, type, or variable.

But Cpp2 is context-free. If A in A(B) is a type, it would allocate memory, but if A is a function, it won't do that. Is this behaviour context-free?

@JohelEGP
Copy link
Contributor

Grammatically, A in A(B) is just an identifier. So I guess it's a matter for semantic analysis.

An example where grammar matters is the t in x is t.
t is grammatically a type, so you need parentheses to make it an expression: https://cpp2.godbolt.org/z/Me1Yh588n.

@msadeqhe
Copy link
Author

Thanks for info. I don't have enough knowledge about how compilers and transpilers work, but 2.Gram() looks strange to access a nested type from literal 2 and call it as if it was a member function, while thinking in UFCS which is about free functions and member functions.

@JohelEGP
Copy link
Contributor

It also works with function objects: https://cpp2.godbolt.org/z/44zGx8TMq.

@msadeqhe
Copy link
Author

msadeqhe commented May 13, 2023

That's right. Function objects object(1) are inherently functions, as how object[1] looks like arrays. Although Type() in Cpp1 is a function-style way to call constructors, AFAIK it is not working a part of syntax in Cpp2, therefore UFCS shouldn't work for types in Cpp2.

@JohelEGP
Copy link
Contributor

JohelEGP commented May 13, 2023

See #193 (comment) starting from "Re function-style construction notation:".

Quote

Re function-style construction notation:

b := std::vector<int>(5, 1); // vector of five ones

Yes, that happens to work (though I didn't initially intend it) because C++ naturally allows a type name to be used as effectively a callable entity, with the semantics of invoking the constructor. I could go out of my way to prevent it by emitting all nonmember function calls as a macro that uses if constexpr to test whether the id-expression is a type, but for now it doesn't seem worthwhile to ban it.

The :-consistent way of writing an expression scope (temporary) object would be as an unnamed object :sometype = (some, args), much like expression scope unnamed functions. For now I've deliberately disabled that so it errors out if you try to write it, but it would be the main alternative.

-- Extract from #193 (comment).

@msadeqhe
Copy link
Author

Thanks. So eventually Type(...) will be banned in Cpp2.

@hsutter
Copy link
Owner

hsutter commented May 15, 2023

Note that in expressions cppfront currently supports both :Type = (some, args) and Type(some, args).

Although I didn't initially intend the latter function-style use, I haven't seen anything inherently wrong with it, and so I've left it in because it seems useful. This thread shows another useful case... I don't think 2.Gram() is too surprising, is it?

At first blush, it seems clean and general to me that Type(42) and 42.Type() both work, and it seems potentially useful.

@msadeqhe
Copy link
Author

msadeqhe commented May 15, 2023

It's surprising to me, because:

  1. UFCS is about Unifying Function Call Syntax, and suddenly it works with types.
  2. It doesn't feel expressive enough for a context-free language. : Type = (args) creates a variable, but A(args) (also a.A(args)) may create a variable or may call a function (or function object).
  3. It's like accessing a base class within multiple inheritance, e.g. a.Base::call() in Cpp1.

@msadeqhe
Copy link
Author

msadeqhe commented May 15, 2023

  1. It's like accessing a base class within multiple inheritance, e.g. a.Base::call() in Cpp1.

Also it would conflict with multiple inheritance in Cpp2, it depends on how we would access base types:

Base1: type = {
    operator=: (out this) = {}
    operator=: (out this, v: x) = {}
    operator(): (this) -> int = 0;
}

Base2: type = {
    operator=: (out this) = {}
    operator=: (out this, v: x) = {}
    operator(): (this) -> int = 0;
}

x: type = {
    this: Base1 = ();
    this: Base2 = ();
    variable: Base1 = ();

    operator(): (this) -> int = {
        // It calls operator().
        m: = this.variable();

        // Does it call operator() from Base1?
        // or calls the constructor with `Base1(this)`?
        // It's ambiguous because of UFCS on types.
        n: = this.Base1();

        return 0;
    }
}

In example above, this::Base1() can be another syntax option, but that resembles scope resolution operator (e.g. namespace::... or type::...) which doesn't look uniform to how we access members of this.

It would complicate the language, similar to how Type(...) has complicated Cpp1 for object construction and function declaration in Most Vexing Parse. It's better to distinguish types from functions and variables syntactically in addition to semantically.

@msadeqhe
Copy link
Author

msadeqhe commented May 16, 2023

It's surprising to me, because:

  1. UFCS is about Unifying Function Call Syntax, and suddenly it works with types.
  2. It doesn't feel expressive enough for a context-free language. : Type = (args) creates a variable, but A(args) (also a.A(args)) may create a variable or may call a function (or function object).
  3. It's like accessing a base class within multiple inheritance, e.g. a.Base::call() in Cpp1.

I couldn't find main reasons that why it's surprising to me. Now I've found them:

  1. UFCS is syntactically and semantically incorrect for types.
    • UFCS is about to unify function(a, args) (non-member functions) with a.function(args) (member functions).
      • It's important to note that both of them are valid syntax for functions without UFCS.
    • On the other hand, Type(a, args) is unified with a.Type(args).
      • But the problem with a.Type(args) is that itself is not a valid syntax without UFCS!
      • It must be A::Type(args) to be a valid nested type, because nested types always need scope resolution operator.

So UFCS on types would unify Type(a) (object construction) with an invalid syntax a.Type() (nested type which has to be A::Type()). That's the reason why I think UFCS on types are incorrect.

  1. It's inconsistent with nested types, thus what's the point of UFCS on types? For example:
A: type = {
    X: type = {}
}

B: type = {
    operator=: (out this, a: A) = {}
}

main: () = {
    a: A = ();

    // It works.
    // It's equal to `B(a)`.
    m: = a.B();
    // a.B() == B(a)

    // ERROR! It doesn't work.
    // It must be `A::X()`.
    n: = a.X();
    // a.X() != A::X(a)
}

So a.Type(arg) would lead to surprises on types, because it doesn't work on nested types.

UFCS on types is in contrast to the purpose of operator. which is to access members!

@SebastianTroy
Copy link

SebastianTroy commented May 16, 2023 via email

@AbhinavK00
Copy link

Type(...) has always been surprising to me (since the first time I saw it way back) because types are not callable but then I always viewed it as functions having the name "Type".

@msadeqhe
Copy link
Author

msadeqhe commented May 16, 2023

  1. UFCS is syntactically and semantically incorrect for types.
    ...
  2. It's inconsistent with nested types.
  1. UFCS on types would make member functions to conflict with a.SOMETHING(args).

Member functions and types are completely different, but unwillingly they will impact each other. It's is in contrast with UFCS for functions in which it only impacts on what function to call.

abc: type = {
    klass: (this) = {}
}

klass: type = {
    operator=: (out this, v: abc) = {}
}

main: () = {
    a: abc = ();

    // It conflicts...
    // Does it call the constructor of `klass`?
    // or it calls the member function `klass`?
    a.klass();
}

In this example, the meaning of a.klass() would be ambiguous.

Isn't a constructor just a function that returns an instance of a type?

@SebastianTroy, That's not the whole story. It has side effects especially when UFCS comes in. I think you read my comments via email, therefore you didn't read my examples because I've edited my comments and added them later.

Type(...) has always been surprising to me (since the first time I saw it way back) because types are not callable but then I always viewed it as functions having the name "Type".

@AbhinavK00, I hope they would be changed to (...)TYPE as suggested in this issue.

@msadeqhe
Copy link
Author

msadeqhe commented May 16, 2023

  1. UFCS on types would make member functions to conflict with a.SOMETHING(args).
  1. For any type named klass, semantically a.klass() is inconsistent with member access.

In contrast, o.func() and func(o) for functions are semantically consistent with member access, the first argument is the object.

But a.klass() and klass(a) for types are semantically inconsistent with member access, the first argument is not the object, it's just an argument which shouldn't be used like an object. Types don't have enough relation to UFCS.

a.Type() --> operator=(out this, a)
a.func() --> func(a) // `this = a` for member functions

@JohelEGP
Copy link
Contributor

There is a difference between 2.Gram() and actual initialization syntax
(not that 2.gram(), with a function, performs any better).
The former lowers to copy-initialization, while the latter lowers to list-initialization.

@msadeqhe
Copy link
Author

@JohelEGP That's an interesting implementation detail.

@msadeqhe
Copy link
Author

msadeqhe commented May 21, 2023

Type(args) is Cpp1-style name for constructors, because:

  • The name of constructors is the same name of their outer type in Cpp1.
    So it's consistent to write Type(args) to call the constructor in Cpp1 without UFCS:
    // Cpp1
    class Type {
        public:
        Type(/*args*/) { /*statements*/ }
    }
  • But the name of constructors is operator= in Cpp2. We don't need to use Type(args) in Cpp2.

@hsutter, there are some discussion about a.Type(args) in this issue.

The following paragraphs are the related parts of my comments from there to here.

Inherently types are not callables, they are block of related declarations. Whereas their constructors are callable, but their this parameters don't exist yet. So UFCS is not for them. Briefly, the problems with a.Type(args) in Cpp2 are that:

  • Syntactically it's inconsistent with member access operator (aka operator dot).
    • Because a doesn't have member Type.
  • Syntactically it's inconsistent with scope resolution operator (aka ::) for referring to types.
    • Because nested types are not instance members.
  • Syntactically it's wrong, because a is not the first argument of Type's constructor in operator=: (out this, args).
    • In a.func(args) for UFCS on functions, a is the first argument which is:
      • inout this
      • in this
      • copy this
      • move this
      • forward this
    • In a.Type(args) for UFCS on types, a is not the first argument of the operator= which is:
      • out this
  • Syntactically it's not context-free.
    • The compiler (not transpiler) and the programmer must look up for Type declaration to see if that's a type or a callable.
  • Semantically it's inconsistent with UFCS, because they do completely different things, in this way:
    • In a.func(args) for UFCS on functions, a is the object to work with it.
    • In a.Type(args) for UFCS on types, a is not the object to work with it.
      • a and args are arguments to construct a new object.
    • The interface of object for a.Type(args) is not intentional, but it maybe is intentional (possibility) for a.Type() (the operator= with only 1 parameter) and a.func(args):
      Type: type = {
          operator=: (out this, a: int) = {}
          operator=: (out this, a: int, b: int) = {}
      }
      
      func(inout value: Type, a: int, b: int) -> Type = { /*statements*/ }
      
      xx: = 10.Type(10);     // Is it intentional?
      ab: = 10.Type();       // OK. It maybe is intentional (possibility)
      yy: = ab.func(10, 10); // OK. It maybe is intentional (possibility)
  • Semantically it's misleading and meaningless, because a has always exactly the same behaviour as args.
    • That's a useless visual separation.
      // This separation between `true` and other arguments are misleading and meaningless.
      // Using TYPE(...) and UFCS on types for object construction:
      x0: = true.Connection(2000, my::proxy());
      
      // All arguments have the same behaviour for object construction.
      // Using (...)TYPE for object construction:
      x1: = (true, 2000, ()my::proxy)Connection;

To make this behaviour expressive enough, the syntax for calling constructors have to be different. The most similar concept to constructors are UDLs, therefore constructors can be called like they are generalized simpler UDLs.

On the other hand, (args)Type is consistent with left-to-right grammar of Cpp2, because the type is after arguments. Also (args)A is context-free as opposite to A(args) in which A can be either function, function object or type.

@msadeqhe
Copy link
Author

C++23 have both static operator() and static operator[].

@hsutter, by making the syntax of object creation from Type(args) to something else like (args)Type, (args):Type, (args).Type or etc, it would be possible to call static operators () and [] directly:

Ab: type = {
    // static operator()
    operator(): (i: int, j: int) -> bool = i == j;

    // static operator[]
    operator[]: (i: int, j: int) -> bool = i == j;
};

ab: Ab = ();

m: bool = Ab(1, 2); // It calls `static operator()` directly.
n: bool = ab(1, 2); // It calls `static operator()` too.

x: bool = Ab[1, 2]; // It calls `static operator[]` directly.
y: bool = ab[1, 2]; // It calls `static operator[]` too.

@JohelEGP
Copy link
Contributor

To enable authoring the gram from #284 (comment) as a library,
we will need Cpp2's equivalent of Cpp1's using namespace.

zaucy pushed a commit to zaucy/cppfront that referenced this issue Dec 5, 2023
… UFCS on numeric literals

(1) I find `1.0 and `1.0f` clearer than `1.` and `1.f` anyway, so that's a small plus.
(2) This was the only thing in the way of using UFCS on numeric literals, so that's another maybe-medium plus.

And the latter enables writing unit libraries like `42.gram()`, as proposed in hsutter#284's comment thread, which is an interesting use of UFCS on literals. So I'll mark this as closes hsutter#284.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants