-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
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
[RFC] add isDefault
; more general than isNil, isEmpty etc
#13526
Conversation
RFC process would be appreciated. I personally prefer |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This makes sense for enums
but apart from that I am skeptical, at the very least [0].isDefault
returning true
doesn't make sense to me.
Whatever is decided for the semantics of isDefault
, an isEmpty
should also exist for string
and possibly cstring
and seq
(and probably other collection types). In fact, why don't we just define it for any type that defines a len
proc?
Yeah, that nails it. |
I've added [RFC] in the title; having this discussion here or under https://github.com/nim-lang/RFCs/issues doesn't make a big difference but I can add a reference issue that simply forwards to here if needed
it's not about the name it's about the underlying semantics, see top post for important differences
let's say this: template isEmpty*[T](a: T): bool = a.len == 0
isEmpty opens a can of worms especially for reference/pointer types eg:
most of the time when you deal with ptr types (eg cstring), the 1st thing you care about is whether they're nil, not whether As I mentioned above, you'll also have to overload isEmpty for certain types, otherwise performance will be bad as shown below, for any type where
|
isDefault
; more general than isNil, isEmpty etcisDefault
; more general than isNil, isEmpty etc
Well obviously |
|
In my non-existing, alternative RFC |
|
But writing |
I don't think there's anything wrong with "too general" in this case, but if you care about having something more restrictive we just may need both here's a plausible version of I verified that both the template isDefault*[T](a: T): bool =
a == default(type(a))
when false:
# pending `enableif` PR https://github.com/nim-lang/Nim/pull/12048
template isEmpty*[T: object|seq|string|set](a: T): bool {.enableif: a.len is int .} =
a.len == 0
else:
# use concepts
type HasLen = concept x
x is object|seq|string|set
x.len is int
template isEmpty*[T: HasLen](a: T): bool =
a.len == 0
# still inefficient for types where `len` is not O(1) so needs specializations
type Foo = object
x: int
type Foo2 = object
x: int
proc len(a: Foo2): int = a.x
doAssert not isEmpty(@[""])
doAssert isEmpty(@[""].type.default)
doAssert not isEmpty("abc")
doAssert isEmpty("")
doAssert not compiles(isEmpty(Foo()))
doAssert not isEmpty(Foo2(x: 2))
doAssert isEmpty(Foo2())
##
## edge cases
##
## edge case 1:
type Foo3 = object
capacity: int
initialized: bool
len: int
doAssert isEmpty(Foo3(capacity: 10, initialized: true, len: 0))
## edge case 2:
doAssert not compiles(isEmpty([1,2]))
# this is an edge case; it's result depends on type, not runtime value,
# so having it not compile is sensible; there are arguments in opposite direction
## edge case 3:
doAssert not compiles isEmpty((x: "foo", len: 0)) # should tuples be supported?
## for ref objects, `isEmpty` must use a deref; `isDefault` can be used both
## on the ref (checks nil-ness) and the deref (checks empty-ness)
import tables
var t = newTable[int, int]()
doAssert not compiles(t.isEmpty)
doAssert not t.isDefault
doAssert t[].isEmpty
doAssert t[].isDefault
t[1]=1
doAssert not t[].isEmpty
doAssert not t[].isDefault
var x: set[int8]
doAssert x.isEmpty
doAssert x.isDefault |
Why don't you just use |
it's explained in top post. foo[2].bar() == default(type(foo[2].bar())) # not DRY
vs:
foo[2].bar().isDefault and "knowing" that bar() is of some type foo[2].bar() == default(Bar[string, float])
vs:
foo[2].bar().isDefault or when And writing something like: let x = foo[2].bar()
x == type(default(T)) can change the semantics, eg cause a copy, which is obviously worse. |
Why again do you need this to be in system.nim? Why don't you just declare |
the need for such sugar is often request feature (see top post, eg it's a trivial function, but one that can be expected to be used a lot, so it makes sense to defined in stdlib (right where if system.nim is no good, can you suggest another module? sugar would make sense but carries along dependencies (macros, typetraits, underscored_calls) And if no module is suitable, I'll go ahead and close this PR (but I don't think adding |
@timotheecour unlike you, I don't see the use case for |
Sorry, rejected before we all spend even more time arguing about it. |
I'd like to revive this in light of #16191 (comment) with the following modification:
|
Adding a 3rd variant when there is no consensus on the existing 2 variants isn't all that helpful. |
this is an often requested feature [1], the lack of which forces some packages to define their own flavors eg:
isDefault
is more useful thanisEmpty
IMO
isDefault
is the more useful abstraction in the majority of cases, and is more general thanisNil
, or something likeisEmpty
(which has been proposed a few times) etc.isNil
andisEmpty
don't cover important use cases, eg:isEmpty
is usually the wrong abstraction:isEmpty
is always false forarray[N, T]
variables (with N>0), so it's not particularly useful here.for
cstring
, most algorithms I can think of care about whether a variable is nil or not, not whether it's len (c_strlen) is == 0isDefault
is automatically defined for any type that has==
definedwhereas it's much harder to define
isEmpty
generically, let alone doing so efficiently eg many types would need a custom definition (Tables, etc)eg: the naive
is not efficient for some types where computing
len
is O(n) or expensive (eg linked lists,intsets
,packedjson
, c_strlen for cstring, etc)"".cstring
empty or not?); it would always give rise to debates about what's consideredempty
, unlikeisDefault
.semantics
isDefault
doesn't try to be too clever about what is considered "default", instead it just uses system.default(T) for that.seq
andstring
. More overloads can be added in other modules as needed, when optimization dictates it. Note that such overloads should rarely be needed, because most types would often have effcient==
definedisDefault
is NOT binary equality, eg 0.0 and -0.0 aren't binary equal, but they're semantically (==
) equal, henceisDefault(-0.0)
is trueisDefault(initTable[int,int]())
is true despiteinitTable[int,int]()
not being binary equal to t2 withvar t2: Table[int,int]
: what matters is semantic equalityisDefault(newTable[int,int]())
is false andisDefault(t3)
is true witht3: TableRef[int,int]
Examples 1:
see
runnableExamples
in PRExample 2: enables DRY code
[1] requests for a similar feature
I've seen it a few times already in gitter eg
*
note
notDefault
as sugar overnot isDefault
to avoid usingnot isDefault
(same rationale asisnot
vsis
); a few ppl have mentioned the needname-wise,
notDefault
is preferable toisAny
because it's self documenting, whereasisAny
would be counter-intuitive for things likedoAssert not [0].isAny
(see above discussion about isEmpty)default
is defined, and it avoids having to import a heavy dependency like sugar etc for something that ought to be common