nim-lang / RFCs

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

Allow to initialise Named Tuples with same notation as Objects #321

Closed al6x closed 1 year ago

al6x commented 3 years ago

It would be convenient to support both tuple and object initialisation for Named Tuples.

type Person = tuple[name: string]

echo Person(name: "John")

1) Frequently code that uses tuple doesn't need to know how it's implemented - if it's an object or tuple. Say I decided to change Person from object to tuple - and now I have to update tons of old code Person(name: name).

2) It just feels right and uniform.

greenfork commented 3 years ago

Could you share the use case where you prefer to use tuples instead of objects and therefore use this initialization syntax?

Currently it's also possible to initialize tuple like this: (name: "John").Person

In my experience tuples are clunkier to operate when used extensively:

I use objects and when I need a short notation for e.g. Point I write a converter that transforms tuple to object example

al6x commented 3 years ago

Could you share the use case where you prefer to use tuples instead of objects and therefore use this initialization syntax?

Like this one:

type Cash* = tuple[amount: float, currency: string]

let cash = Cash(amount: 1.0, currency: "USD")

I like ability to use shortcut initialisation like (1.0, "USD") but it feels strange and wrong that I can't use the normal initialisation Cash(amount: 1.0, currency: "USD").

P.S.

Some more cases

Price* = tuple[price: float, currency: string, time: Time]

type Point*  = (Time, float)

# With some conversion magic
let prices: seq[Point] = @[
  ((1990, 1, 1), 1.0),
  ((2000, 1, 1), 1.0), ((2000, 1, 2), 3.0),
  ((2000, 2, 1), 1.0)
]

Currently it's also possible to initialize tuple like this: (name: "John").Person

But why? Why add more and more ways to do the same thing? Wouldn't it be better and simpler to write Person(name: "John")? Compiler knows that Person is a tuple and has no problem figuring out how to resolve it.

greenfork commented 3 years ago

I can't answer "why" exactly but I have several thoughts about your use case.

I would use objects instead of tuples every time. Tuples have 2 benefits:

  1. Shorter notation (1.0, "USD") instead of Cash(amount: 1.0, currency: "USD")
  2. Simpler equivalence rules: any tuple with (amount: 1.0, currency: "USD") fields and values is equal to any other tuple, no need to construct a specific object for comparison

1st one is remedied by the use of converters, you can achieve any level of convenience in code. 2nd one is debatable at best especially considering that you need to do financial stuff. So to me it looks like "ugliness on purpose" that you shouldn't really use tuples for these use cases.

But the general idea about consistency with object initialization looks good. I don't see a use case where I explicitly need to know that this is object, not tuple construction.

disruptek commented 3 years ago

The real problem with using tuples instead of objects is that they expose field-order-based iteration which some knuckehead is going to end up depending upon. As soon as you want to upgrade to an object, you break that code.

I really don't understand why people want to typedef tuples; to me it's code smell every time.

saem commented 3 years ago

The real problem with using tuples instead of objects is that they expose field-order-based iteration which some knuckehead is going to end up depending upon. As soon as you want to upgrade to an object, you break that code.

Isn't the real answer here to instead to create field iteration differences rather than initialization differences? The the current approach is creating a referred pain at some initialization site as opposed to the site of any traversal (or parameter) that may be traversal order dependent.

liquidev commented 3 years ago
type Cash* = object
  amount: float
  currency: string

proc usd(amount: float): Cash = Cash(amount: amount, currency: "USD")
let cash = 1.usd

This is a style I've adopted into my code, it works pretty well, and I'd argue it looks much better than your (1.0, "USD"). If you need a lot of currencies, you can always create a sugar template for defining them:

import std/strutils

template currency(name: untyped) =
  const currencyName = astToStr(name).toUpperAscii
  proc `name`(amount: float): Cash {.inject.} =
    Cash(amount: amount, currency: currencyName)

currency usd
currency eur
disruptek commented 3 years ago

I agree that I'd rather have object/tuple construction that wraps identdefs and I'd rather have consistent syntax, but to me, it's more important to improve the documentation on tuples to prevent people from thinking that they are somehow buying themselves anything more than future grief.

Have fun adding a field to that tuple; you still have to change all your initializations...

al6x commented 3 years ago

There are different ways tuples could be used or even avoided.

But why have different syntax for it? Wouldn't it be better to keep syntax same and uniform?

P.S.

The real problem with using tuples instead of objects is that they expose field-order-based iteration

I think it's ok and convenient to use positional unpacking for tuples of size of 2-3, the problem you mentioned usually arises when the tuple size is >3.

And, maybe some day Nim would support name based unpacking for tuples and objects. So people would use key unpacking let { b, d } = obj instead of positional unpacking let (_, b, d) = obj.

liquidev commented 3 years ago

And, maybe some day Nim would support name based unpacking for tuples and objects. So people would use key unpacking let { b, d } = obj instead of positional unpacking let (_, b, d) = obj.

In my opinion a better syntax that would actually fit the language would be let (field1: a, field2: b) = obj.

disruptek commented 3 years ago

The real problem with using tuples instead of objects is that they expose field-order-based iteration

I think it's ok and convenient to use positional unpacking for tuples of size of 2-3, the problem you mentioned usually arises when the tuple size is >3.

Unpacking is yet another problem, but no less annoying to deal with regardless of how large your tuples are. It has the feature that you must hope that the names and types of the fields you're unpacking will not change, otherwise, you can't trust that your code will remain correct. Yet another reason not to use tuples except in very limited circumstances.

This is a partial solution: https://github.com/disruptek/foreach

It should probably be modified to simply use the identdef syntax as @liquidev proposes; maybe it should desugar assignments as well.

Araq commented 1 year ago

Rejected. Nobody is working on it and it's hardly important. Just start with object when modelling your domain and move less often from tuple to object.