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

[superseded] alias: myecho=echo to alias any symbol #11822

Closed
wants to merge 11 commits into from

Conversation

timotheecour
Copy link
Member

@timotheecour timotheecour commented Jul 25, 2019

[EDIT] nim now has a way to alias any symbol (for lvalue expressions, see #11824 instead), for example alias: myecho=echo.
This has been an often requested feature, found in other languages eg D's alias (or to some extend C's define)

see extensive unittests in tests/magics/talias.nim

  • the alias only works by design with expressions that resolve to symbols, and gives sane CT error if it can't
  • the alias works fine with overloaded symbols
  • it works with any symbol: module, template, macro, type, const, var, proc, iterator etc (EDIT: eg alias:r=result works just fine)
  • the usage of the alias is identical to the usage of the symbol being aliased

fixes

  • using alias avoids the issues mentioned in Internal compiler error for proc alias #8935: alias:testForMe=assert works, alias:myPuts=system.echo works, generic/template/macro aliases work etc
  • fixes Unable to alias echo from the system module #11731: alias:my_echo=system.echo works
  • fixes alias declarations #7090 ; this was wrongly closed: the workarounds didn't work in many cases, see test suite; eg
    • templates / macros/ iterators with all optional params didn't work
    • a macro with varargs; eg: template myalias(args: varargs[untyped]): untyped = echo(args) didn't work with myalias()
    • module didn't work (requires a different syntax, import foo as bar)
    • would require different syntaxes for different symbols (eg proc vs let, or depending on whether template/macro being aliased has some argument or not)

this feature request keeps popping up

proc getLength(i: int): int = sizeof(i)
proc getLength(s: string): int = s.len
# const length = getLength # Error: cannot generate VM code for getLength
alias:length = getLength # works
echo length("alias")

This is my biggest annoyance with nim right now

examples (see more in tests/magics/talias.nim)

alias:echoa=echo
echoa (1,2)

template fun(a = 10, b = 11): untyped = (a,b)
alias:funa=fun
doAssert funa() == (10, 11)
doAssert funa == (10, 11)
doAssert funa(3) == (3, 11)

alias:toStr=`$` # works fine with overloads
doAssert toStr(true) == "true"

alias:system2=system # works fine with module aliasing
alias:toStr2=system2.`$` # that works too
doAssert toStr2(true) == "true"

could it have been done without patching the compiler ?

no.
There are many issues making that impossible without this PR, if you're not convinced, try out the test suite I have (for example, aliasing a template/macro with all optional params, iterator, module etc). Using things like template myalias(args: varargs[untyped]): untyped = myexpr(args) or more elaborate variants just doesn't work in many situations; eg: template myalias(args: varargs[untyped]): untyped = echo(args); myalias() # fails;

Furthermore, the patch is small and straightforward (essentially just introduces a new magic), and results in essentially no compile time overhead; it really boils down to adding addInterfaceDecl(c, alias), see semAlias

note

see also #11824 which defines ref for lvalues instead of alias for symbols. Lvalues and symbols should not be conflated as it would lead to ambiguities, hence the 2 different syntax:

alias: sym2=sym # a symbol (this PR)
byRef: myref=sym[2].bar.baz # an lvalue (pure library solution, see https://github.com/nim-lang/Nim/pull/11824)

use cases

the main use case for alias is as described in https://dlang.org/spec/declaration.html: a way to redirect references from one symbol to another

  • conditionally override a symbol, eg:
when defined(overrideEcho):
  alias: myecho = myechoImpl # also logs, adds file/line numbers etc
else:
  alias: myecho = echo # myecho un-distinguishable from echo

(note that template myecho(args: varargs[untyped]) = echo(args) won't work, as noted above)

  • conditionally enable a symbol, eg: assert / doAssert could've been implemented in a simpler way:
template doAssert*(cond: untyped, msg = "") = ...
when compileOption("assertions"):
  alias: assert*=doAssert
else:
  template assert*(cond: untyped, msg = "") = discard

this would've avoided the complications due to instantiationInfo being affected by intermediate template definition, which alias avoids.

Another similar scenario is conditional definition, eg os.quoteShell which could've been implemented with alias and avoid intermediate proc + non DRY code

  • avoid a symbol clash:
  import logging
  alias: debug2 = debug # suppose `debug` clashes
  var logger = newConsoleLogger()
  addHandler(logger)
  debug2 (12, 13)
  debug2()

note that template debug2(args: varargs[untyped]): untyped = debug(args) would not work, giving CT error.
Also note that when implementing an alias via an intermediate template in cases where it does work, it doesn't quite fit as a 1-1 replacement, eg reflection using macros.owner, macros.getImpl will produce different results, unlike with alias (see also note regarding instantiationInfo)

other use cases:

  • alias also allows to avoid symbol clashes while keeping method call syntax, eg when doing
from foo import nil # to avoid putting too many clashing symbols in scope
alias: bar2 = foo.bar # bar2 still usable with method call syntax
  • turn a regular proc into an operator / special name so that a generic code works, eg:
# mylib.nim
proc toStr*(a: Foo): string = ...
# main.nim
import mylib
alias: `$` = toStr
echo Bar # somewhere inside Bar, `toStr(Foo)` will be used automatically thanks to `$`

ditto with turning regular third party procs into operators, eg matrix operations:

proc matMul(a, b: Mat): Mat = ...
alias: `*` = matMult
echo mat1 * mat2 * mat3

how frequent are aliases?

pretty frequent, search in nim code base for alias.
eg:

proc contains*[T](c: CritBitTree[T], key: string): bool {.inline.} =
  ## returns true iff `c` contains the given `key`.
  result = rawGet(c, key) != nil

proc hasKey*[T](c: CritBitTree[T], key: string): bool {.inline.} =
  ## alias for `contains`.
  result = rawGet(c, key) != nil

this could be rewritten as:

# more declarative, more obvious it's just an alias
alias: hasKey*=contains # critbits.nim 
alias: fmt *= `&` # strformat.nim
alias: `$` *= renderSQL # parsesql.nim

there are many examples of that in nim code, and even more in third party libs.

alias in other languages

(not restricted to types)

@timotheecour timotheecour changed the title nim has a real alias nim has a real alias that works with any symbol Jul 25, 2019
This was referenced Jul 25, 2019
@jcosborn
Copy link
Contributor

I'm already using := extensively in my code, so this would break a lot.
How about something like declareAlias(sym, expr)? This could also be written as

declareAlias(sym):
  expr

Or maybe it is possible to reuse bind somehow

bind sym = expr

tests/magics/talias2.nim Show resolved Hide resolved
lib/system.nim Outdated Show resolved Hide resolved
@Araq
Copy link
Member

Araq commented Jul 25, 2019

This has lots of issues:

  1. As others remarked, at this point in time adding a new operator to system is unacceptable.
  2. skAlias was on its way out of the compiler, you re-introduced it and that means it adds ongoing costs to maintain it. Whereever we do s.kind == skWhatever in the compiler it could introduce a new bug because we didn't consider to skip skAlias, the compiler is already full of this cruft, it's terrible and you just made it worse.
  3. Likewise, you might not skip skAlias too eagerly in the compiler because then the error messages don't mention the "real" symbols.
  4. Aliases are bad style and not to be encouraged.

@sschwarzer
Copy link
Contributor

4. Aliases are bad style and not to be encouraged.

Why are aliases bad style? I guess it also depends on the use case. What are situations where you would consider it bad style and where would they be more appropriate?

As far as I'm concerned, I was thinking of naming subexpressions, but these extra assignments would probably/hopefully be "optimized away" by the compiler anyway. :-) For the subexpression case a compact syntax would be nice, otherwise I'd probably just accept potentially having an extra copy operation for the subexpression assignment.

@Araq
Copy link
Member

Araq commented Jul 25, 2019

Why are aliases bad style?

Because you always have to learn them, so if there is os.fileExists and os.existsFile two things could be going on:

  • There is some subtle difference between these two. (Bad!)
  • They are the same (better). But then why do both exist?

It's pointless cognitive overhead.

As far as I'm concerned, I was thinking of naming subexpressions, but these extra assignments would probably/hopefully be "optimized away" by the compiler anyway. :-) For the subexpression case a compact syntax would be nice, otherwise I'd probably just accept potentially having an extra copy operation for the subexpression assignment.

Oh, that's a different kind of alias.

@zah
Copy link
Member

zah commented Jul 25, 2019

I think aliases can reduce cognitive overhead by not asking you to remember non-important details that tend to vary among langauges. (was it list.add or list.append?). Their downside is that they promote a slightly messier and inconsistent code where you can recognize how the author is coming from lang X or Y.

@disruptek
Copy link
Contributor

Who gets to decide whether two concepts are semantically identical between languages? What if they get it wrong?

Why should I have to guess as to whether you've reimplemented something or merely aliased it?

I'm not sure how I feel about using, but this seems gratuitous at best. Nim should not stoop to make itself look like another language for the benefit of one writer at the expense of every subsequent reader.

@krux02
Copy link
Contributor

krux02 commented Jul 25, 2019

New library features should go to lib/experimental, and new programming language features should only be available when explicitly enabled via compilation flag.

@dom96
Copy link
Contributor

dom96 commented Jul 25, 2019

You've created two PRs that implement the same thing, why don't you make that evident in the title of the PR? or even at the start of your PR text?

The current title could be better, and would allow me and others to quickly see what this PR is about. I would name it like this: "Implements := operator that acts as an alias (alternative to #11824)". Your current title is false for many reasons:

  • It stipulates that Nim has a feature
  • It talks about alias which imples an alias keyword being added, but that isn't the case with this PR.

Once again, please write more concisely and more clearly.


As far as this PR goes: the := shouldn't be used for something as trivial as this. I prefer this PR: #11824.

@timotheecour
Copy link
Member Author

New library features should go to lib/experimental, and new programming language features should only be available when explicitly enabled via compilation flag.

done, you can now enable the feature via a localized flag:
{.push experimental: "aliasSym".}

As others remarked, at this point in time adding a new operator to system is unacceptable.

done, moved to sugar.nim

will address other points in a bit

@timotheecour timotheecour changed the title nim has a real alias that works with any symbol [feature] new := operator that can alias any symbol Jul 26, 2019
@timotheecour
Copy link
Member Author

timotheecour commented Jul 26, 2019

You've created two PRs that implement the same thing

They're not the same thing:

  • [superseded] ref syntax for lvalue expressions: byRef: myref=x[1].foo[2] #11824 defines lvalue expression references, exactly like C++'s ref (auto &x = expr(...);): it evaluates a runtime expression that has an address and returns it by reference
  • this PR defines symbol aliasing (eg iterator, macro, templates, procs, variable, module, etc), exactly like D's alias, and somewhat achievable using C's #define: it defines an alternative name to refer to a symbol.

These have entirely different properties:

  • references are for runtime values (eg, could throw an exception during evaluation), they occupy a slot in generated C code that can increase stack size in a procedure
  • symbol aliases operate on compile time entities only, they will never throw an exception, there is no address involved, they only affect symbol lookup.

why don't you make that evident in the title of the PR? or even at the start of your PR text?

I've improved the title. I had mentioned in the 1st sentence that this PR was for aliasing symbols, whereas #11824 was for lvalue expressions. I've reworded a bit, I hope it's clear enough now.

The current title could be better (...) Implements := operator that acts as an alias (alternative to #11824) (...)

I've improved the title. This PR however is not an alternative to #11824, as explained above.

As far as this PR goes: the := shouldn't be used for something as trivial as this. I prefer this PR: #11824.

I moved it to sugar.nim. As I've explained in the top post, it is impossible AFAIK to alias symbols using a pure library solution, so I'm not sure what you mean by trivial; in any case, #11824 doesn't help / can't be reworked to work for symbols (eg iterators, etc); if you disagree, please propose something and run it against the test suite

I will address remaining points later, notably regarding use cases that are made difficult without this feature

@sschwarzer
Copy link
Contributor

@dom96 You suggested a syntax

alias:
  foo = expr

for PR #11824. I think this syntax would make more sense for the feature in this PR. Or even/additionally

alias foo = expr

similar to let foo = expr or var foo = expr?

In that case, as I understand it, there wouldn't be a need to define a new operator that could conflict with other custom operators.

(By the way, I think using alias for PR #11824 would be misleading. Or rather, this PR #11822 fits my understanding of an "alias" much better. :-) )

Copy link
Contributor

@krux02 krux02 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not a fan of introducing new features to the compiler. They always come with new problems that we did not anticipate. But I think to have experimental features that won't interfere with people who don't want to bother with issues of new features they never asked for is ok. And then at a later stage, when we can see for sure that the feature helps real projects without causing too much harm, we can take away the experimental-ness from them.

On the other hand, when this feature causes lots of problems, that we don't know how to fix, then we should be able to revert to a Nim that does not have this feature.

So, the := operator needs to be defined in a new file in lib/experimental e.g. lib/experimental/aliasop.nim, not insugar.nim or any other alread existing file.

lib/pure/sugar.nim Outdated Show resolved Hide resolved
lib/pure/sugar.nim Outdated Show resolved Hide resolved
tests/magics/talias2.nim Show resolved Hide resolved
@Clyybber
Copy link
Contributor

Clyybber commented Jul 26, 2019

Does nim really need this feature?
#8935 only fails for templates which makes sense. If you so badly want to alias procs or templates (which makes whatever code you write harder to understand) you can just do:

template someAlias(x: varargs[untyped])
  theProcOrTemplateToAlias(x)

and maybe introduce an alias macro that generates the above.
For aliasing things like some2DindexableThing[i][j] it makes more sense to just introduce a temporary with let/var.
Because of the above reasons I personally don't feel this feature has a place in the core language,
but maybe it can be implemented via a macro.

@c-blake
Copy link
Contributor

c-blake commented Jul 26, 2019

I am not against the feature per se, but := operator in Python 3.8 (so-called Walrus operator) is an assignment that produces an expression which is a lot different than an alias, and many other languages of the Pascal/Ada lineage use := for just assignment which is also different.

So, if this goes in, I would prefer a different syntax. I like @sschwarzer "alias foo = bar" the best. It seems very Nim-esque syntax to define a new class of thing (macro/template/proc/iterator/type/.../alias). Even the D-inspiring syntax uses a nice alias keyword. @Araq does raise some good points about maintainability, though, and I'm not sure if the few cases not covered by existing mechanisms warrants the maintenance cost of the feature.

@sschwarzer
Copy link
Contributor

sschwarzer commented Jul 26, 2019

I am not against the feature per se, but := operator in Python 3.8 (so-called Walrus operator) is an assignment that produces an expression which is a lot different than an alias

I wouldn't say "a lot". I think there's a good overlap in use cases.

Although I have missed a cheap way of aliasing sub-expression (i. e. without making a copy) in Nim, I nonetheless agree that adding such an aliasing feature may not be worth the extra complexity. It may not be much, but a dozen small simple features can sum up and unnecessarily complicate the language.

This reminds me of another ticket, #8306. I think keyword-only arguments in Python make sense and I use them occasionally in my code, on the other hand I liked the IMHO pragmatic decision not to add them to Nim.

For what it's worth, here the PEP about named sub-expressions for some more background:
https://www.python.org/dev/peps/pep-0572/

@awr1
Copy link
Contributor

awr1 commented Jul 26, 2019

(Non-exported) aliases can be useful in implementations for reducing repetitive, lengthy identifiers down to size. I'm not really a huge fan of aliases as generalized "API convenience utilities" like others have suggested, e.g. append() = add() or normalize() = unitVector().

I agree that it should be a keyword (i.e. alias as others have suggested), as a lot of other languages have alias as a keyword, which makes its intent clear. We should try to make our syntax less misleading to newcomers by virtue of paying attention to how other languages work: e.g. := is the walrus opeartor in Python, and it's also used to instantiate variables in Go; we shouldn't try to further this confusion. We can't pretend like we live in a vacuum.

@dom96
Copy link
Contributor

dom96 commented Jul 26, 2019

#11824 defines lvalue expression references, exactly like C++'s ref (auto &x = expr(...);): it evaluates a runtime expression that has an address and returns it by reference

Right, what confused me is that you spend a lot of your time comparing alias to this byRef you've implemented inside #11824...

@dom96
Copy link
Contributor

dom96 commented Jul 26, 2019

Aliases are amazing, but I will once again repeat the Nim mantra:

A small core language with a powerful set of metaprogramming features.

This is yet another feature that grows the language core which could be implemented using macros (perhaps with some gotchas, but you can get to a pretty good implementation and I bet you can even fix those gotchas with some clever coding). For someone who's dreamt up this mantra I'm disappointed @Araq isn't enforcing it more often.

Nim doesn't need more features such as this, it needs less. Please consider using your time to remove things like the do notation or other features in the experimental manual which should be killed.

@timotheecour I'm not going to close this PR, but I suggest strongly that you do. You will spend time going through the remarks people leave here and then this PR won't be merged anyway.

@sschwarzer
Copy link
Contributor

I actually agree even though I mentioned "alias".

Ironically, I also used the word "alias" - and I used it with a different meaning than suggested in this ticket because it was "obvious" to me what "alias" meant. :-D

@timotheecour timotheecour changed the title [feature] new := operator that can alias any symbol [feature] alias: myecho=echo to alias any symbol Jul 29, 2019
@timotheecour
Copy link
Member Author

timotheecour commented Jul 30, 2019

PTAL:

  • I've added a use cases section in PR top post (TLDR: the motivation for this feature has nothing to do with fileExists vs existsFile; but please read)
  • I've added how frequent are aliases? section and a how frequent are aliases? section on top
  • I've addressed the concern about := being a too generic operator; alias now uses following syntax:
alias: myecho = echo  # alias for symbol (this PR)
byRef: foo2 = x[1].baz # ref for lvalue expression (#11824)

which also makes the syntax more symmetric between byRef and alias. If really needed, I can change that to symAlias: myecho = echo but IMO alias is better, matching both dlang's definition of alias and also, for C++, the definition given here https://stackoverflow.com/questions/9864125/c11-how-to-alias-a-function which shows same difference bw ref and alias as I was explaining earlier.

The reason, features that nobody asked for that are purely based on ideas that one developer things that might be a good idea

that's just not true. This feature request keeps popping up, see links I've added in top PR post
this PR solves this problem once and for all.

@sschwarzer
Copy link
Contributor

sschwarzer commented Jul 30, 2019

@timotheecour Thanks a lot for the additional clarifications!

Nonetheless I can't help it to add a remark on "aliases" in Python (which you mention in the ticket description). The usual form of "aliasing" in Python is just an assignment:

new_name = some_expression
# Use `new_name` instead of `some_expression`.
...

Example:

some_list = [1, 2, 3]
new_name = some_list
# `some_list` and `new_name` refer to the same list object,
# so `some_list` gets `4` added.
new_name.append(4)

On the other hand, this doesn't work as you may expect:

some_list = [1, 2, 3]
new_name = some_list
# This creates a new list and assigns it to `new_name`.
# `some_list` isn't modified.
new_name = [4, 5]

(For details, see this talk, slides 8 and later, but things can get more complicated if customized attribute access is involved.)

The new := operator in Python doesn't change much here. Compared to the regular assignment with = it allows assignments in expressions, which = doesn't allow.

I think conflating this Python semantics of "alias" with other semantics illustrates what I said above about "obvious" high-level concepts vs. subtle semantic differences.

Just to be clear, I'm not saying it would be the end of the world if Nim used the word alias for this feature (if it's merged), but I'd like to mention the problem with the word.

@timotheecour
Copy link
Member Author

timotheecour commented Jul 30, 2019

modeling alias semantics after that of python was never a goal, python is a dynamic language and naturally accepts rebinding of already defined symbols.

There is no conflation or confusion possible here, alias vs byRef are for different things and will give CT errors if mis-used. The doc holds in 2 lines:

  • use alias: foo = sym to alias a symbol
  • use byRef: foo = expr(..) to get a ref to an lvalue expression

this ref/alias distinction uses (AFAIK) the same semantics as in D and some other languages (see note in top post); some language's semantics will defer, obviously, but I'm defining here the alias semantic that IMO makes the most sense for nim.

@sschwarzer
Copy link
Contributor

sschwarzer commented Jul 30, 2019

modeling alias semantics after that of python was never a goal, python is a dynamic language and naturally accepts rebinding of already defined symbols.

I didn't intend to suggest modeling Nim's semantics after Python. I added the comment because you mentioned Python as an example of a language with aliasing, and my point was that the "aliasing" in Python is only used/mentioned informally as such and it's quite limited and has possibly surprising behavior if you think of an assignment as "aliasing".

this ref/alias distinction uses (AFAIK) the same semantics as in D and some other languages (see note in top post); some language's semantics will defer, obviously, but I'm defining here the alias semantic that IMO makes the most sense for nim.

Again, I didn't want to suggest any particular semantics of aliases. My point was more "meta", about different meanings of "alias" and possible misunderstandings for users of the language.

I do appreciate that you make a clear distinction between aliasing of isolated symbols vs. lvalues/refs. 👍

@timotheecour
Copy link
Member Author

timotheecour commented Aug 21, 2019

my other PR #11992 is more general and supersedes this one, making every symbol 1st class (in particular, templates/(inline)iterators/etc can be passed to other routines (procs/iterators/etc)); and defines lambdas and aliases as a by product. I'll keep it open until the other one gets merged (but if really needed I can close this one in the meantime)

@timotheecour timotheecour changed the title [feature] alias: myecho=echo to alias any symbol [superseded] alias: myecho=echo to alias any symbol Aug 21, 2019
@narimiran
Copy link
Member

my other PR #11992 is more general and supersedes this one

I'm closing this one in favor of that one.

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

Successfully merging this pull request may close these issues.

Unable to alias echo from the system module alias declarations