nim-lang / RFCs

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

Check converters at compile time if possible #326

Closed al6x closed 3 years ago

al6x commented 3 years ago

In many cases converters could be checked at compile time.

It should help to decrease runtime errors, improve runtime performance (as conversion done just once at compile time), require no change in the language, and shouldn't slow down compiler (compiler can skip the check if the conversion is too complicated).

It should work for all converters, but I would like to highlight the usage with the example for string enums.

import strutils  # for parseEnum

type
  Priority = enum
    pLow = "low", pNormal = "normal", pHigh = "high"

  Todo = object
    text:     string
    priority: Priority

converter toPriority(s: string): Priority = parseEnum[Priority](s)

echo Todo(text: "Buy milk", priority: "high")      # Simple, should be checked at compile time
echo Todo(text: "Buy milk", priority: "h" & "igh") # Too complicated to check, could be ignored

As a bonus, we would get literal types for string that are fast for free.

Fast because string -> enum conversions will be done at compile time, and during the runtime the enum (in example I use string -> string enum, but it also could be string -> int enum) will be used instead of the string, so all the "literal string" enum comparison operations will be in reality fast int comparisons.

P.S.

And it's aligned with plans for 2021 for Nim - stability and less bugs :).

metagn commented 3 years ago

I believe you want something like this:

converter toPriority(s: string): Priority {.compileTime.} = parseEnum[Priority](s)
# should be similar to how compileTime procs work

var x: Priority
x = "high" # works
let str = "high"
x = str # does not compile

converter toPriorityRuntime(s: string): Priority = parseEnum[Priority](s)
x = str # works

An alternative could be this:

converter toPriority(s: static string): static Priority = static parseEnum[Priority](s)

but this would complicate things.

al6x commented 3 years ago

@hlaaftana thanks, {.compile_time.} is close to what I want, but it doesn't work, try it.

metagn commented 3 years ago

Yes, sorry, I was merely proposing its use here.

Araq commented 3 years ago

So ... sometimes your string literals are checked at compile-time and sometimes they are not? Seems like a really bad idea, sorry. You should use the enum values directly when you can.

al6x commented 3 years ago

sometimes your string literals are checked at compile-time and sometimes they are not?

If it's a simple string literal - it always checked at compile time (don't know if it's possible to implement though).

Seems like a really bad idea, sorry.

Hmm, I look at it a bit differently, from the user perspective. I write a program and I want the program to be correct and bugs detected as soon as possible.

The way it currently works - enum conversion problems would be caught late, when program is running. With this proposal some of those bugs would be detected immediately. So, from my point of view as a user - it is an improvement.

As for how exactly those checks would be implemented - with compiler, or with linter or some other code analysis tools - it's not that important.

Anyway, feel free to close if it's not aligned with Nim design and principles :)

Araq commented 3 years ago

Ah it's just a tooling convenience ... I suppose we can never have enough of these. Still doesn't look particularly important to do, does it?

al6x commented 3 years ago

Still doesn't look particularly important

I guess it depends on the use case, probably not that important if you don't use enums heavily. In my case I use it a lot, let's write stock scanner (we are looking for good mining stocks with put option insurance available) with string and raw enums:

stocks.find do (s: stock) -> bool:
  s.sector          in  ["copper", "silver", "uranium", "gold"] and
  s.stock_risk      ==  "low" and 
  s.country_risk    <   "high" and 
  s.stock_size      in  ["medium", "large"] and 
  s.financial_score >=  "stable" and
  s.option_chain.find_at_least(("put", 0.7, 180)).is_some

With raw enums, it doesn't look that good, and you have to remember all those prefixes.

stocks.find do (s: stock) -> bool:
  s.sector          in  [ssCopper, ssSilver, ssUranium, ssGold] and
  s.stock_risk      ==  srLow and 
  s.country_risk    <   crHigh and 
  s.stock_size      in  [ssMedium, ssLarge] and 
  s.financial_score >=  fsStable and
  s.option_chain.find_at_least((orPut, 0.7, 180)).is_some

And it's also used in lots of other places, when you prepare the data, analyse it, write rules etc, lots of enums.

This feature would allow to write DSL-like nicely looking code easily and validate it's correctness (and if conversion not just checked but also done at compile time it would be also fast).

Araq commented 3 years ago

With raw enums, it doesn't look that good, and you have to remember all those prefixes.

Sure, but you don't have to use the prefixes for raw enums. Nor do you have to use raw enums, you can also use .pure enums.

al6x commented 3 years ago

Yes, I like pure enums, but sadly they still have name conflicts. Maybe instead of this feature it would be possible to improve pure enums so that it would resolve conflicts automatically?

Example:

type
  Color {.pure.} = enum red, orange, yellow, green, light_blue, blue, violet
  RGB {.pure.} = enum red, green, blue

let c: Color = red

P.S.

As far as I know Nim compiler doesn't do backward type inference because of performance reasons, but in that case it looks like just one step lookup, it should be cheap.

Araq commented 3 years ago

What's so annoying about:


let c = Color.red

?

greenfork commented 3 years ago

It's annoying in examples like this one:

proc processInput(w: GLFWWindow) =
  if w.getKey(Escape) == GLFWPress:
    w.setWindowShouldClose(true)
  elif w.getKey(W) == GLFWPress:
    camera.processKeyboard(cmForward, deltaTime)
  elif w.getKey(S) == GLFWPress:
    camera.processKeyboard(cmBackward, deltaTime)
  elif w.getKey(GLFWKey.A) == GLFWPress:
    camera.processKeyboard(cmLeft, deltaTime)
  elif w.getKey(D) == GLFWPress:
    camera.processKeyboard(cmRight, deltaTime)

note GLFWKey.A which has to be prefixed because A is also a gamepad button

EDIT: actually nevermind, it's an artifact of using a wrapper. Using type instead of conversion to int32 should solve it for native Nim programs

bluenote10 commented 3 years ago

This feature would allow to write DSL-like nicely

To me the string based version isn't nicer. What if you want to refactor the names? Refactoring symbols is more obvious and can be assisted by IDEs. With strings you just have lots of hard-coded magic values that may or may not be convertible to enum values.

al6x commented 3 years ago

To me the string based version isn't nicer. What if you want to refactor the names?

@bluenote10 I see, yes symbols would be fine too, if naming conflict would be resolved.

What's so annoying about: let c = Color.red ?

@Araq clean, nice, expressive code paint(red) or company.risk = low looks much better than the paint(Color.red) or company.risk = CompanyRisk.low.

P.S.

Seems like discussion moved away from the string enums toward pure symbol enums, maybe I should close this thread and create another RFS for the automatic conflict resolution for pure symbol enums?

Araq commented 3 years ago

Feel free but I'm about to write an RFC about this as well...

Araq commented 3 years ago

Sorry, I didn't get to write the RFC yet. However, this one here is "rejected".