Hacker News new | past | comments | ask | show | jobs | submit login

Overall this looks nice, but I found myself stumbling over the ToSpan syntax:

    let span = 5.days().hours(8).minutes(1);
It feels sort of weird how the first number appears in front, and then all the other ones are function arguments. I suppose if you don't like that you can just write:

    let span = Span::new().days(5).hours(8).minutes(1);
at the expense of a couple characters, which is not too bad.



I stumbled over

    use jiff::{Timestamp, ToSpan};

    fn main() -> Result<(), jiff::Error> {
        let time: Timestamp = "2024-07-11T01:14:00Z".parse()?;
I seem to remember Rust does that thing with interfaces instead of classes, is it that? How come I import a library and all of a sudden strings have a `parse()` method that despite its generic name results in a `Timestamp` object? or is it the left-hand side that determines which meaning `str.parse()` should have? What if I have two libraries, one for dates and one for say, Lisp expressions that both augment strings with a `parse()` method? Why use this syntax at all, why not, say, `Timestamp.parse( str )`? I have so many questions.


`parse` is actually an inherent method on `str` that always exists: https://doc.rust-lang.org/core/primitive.str.html#method.par...

Its return type is generic, and here it's inferred from the left hand side. It's implemented using the `FromStr` trait, and you can equivalently write `Timestamp::from_str(t)`.

You're thinking of the "extension trait" pattern for using traits to add methods to existing types when the trait in scope, but that's not what's going on here. Jiff's `ToSpan` mentioned above is an example of that pattern, though: https://docs.rs/jiff/latest/jiff/trait.ToSpan.html


All of these options work and are equivalent.

- let time = Timestamp::parse("2024-07-11T01:14:00Z")?;

- let time: Timestamp = "2024-07-11T01:14:00Z".parse()?;

- let time = "2024-07-11T01:14:00Z".parse::<Timestamp>()?;

You’re free to choose whatever you prefer, although the compiler needs to be able to infer the type of time. If it can’t, it’ll let you know.

So a fourth option is allowed, as long as the subsequent lines make the type of time unambiguous.

- let time = "2024-07-11T01:14:00Z".parse()?;

This is a direct consequence of Timestamp implementing the FromStr trait.


  let time = Timestamp::from_str("2024-07-11T01:14:00Z")?;
I think you meant :)


Haha, yes I did. If only the HN textbox integrated with rust-analyzer, it would have caught the mistake.


It’s because Timestamp implements the FromStr trait which is one of the first traits everyone learns about when learning rust. So when you say that your value is a Timestamp and the expression is string.parse()?, the compiler knows that it has to use the implementation which returns a Timestamp.

There will never be two libraries that clash because of Rust’s orphan rule: you can only implement either a trait which you define on any type, or define a foreign trait on a type which you define, so there’s no way for some random library to also ship an implementation of FromStr for Timestamp


Although I sure wish sometimes I could just do that my own binary/non-lib projects


Rust will determine what `parse` does based on the inferred return type (which is being explicitly set to `Timestamp` here). This is possible when the return type has `FromStr` trait.


oh I see, almost what I anticipated


> I have so many questions.

Not being snarky, but I suggest starting by reading at least a little about traits? None of your questions are really about this library - it's just FromStr and an orphan rule.


To make it clear, I didn't want to be the snark either. Just wondered about the usual things like "locally detectable semantics" and so on. I still think `Library.method( argument )` wins over `argument.method()` for the simple reason that the former has `Library` explicitly mentioned. Also, `door.open()`, really? I think `strongman.open( door )` is just that much clearer, flexible and explicit.


Agree to disagree. I always like expressiveness of Ruby, so `2.days` and `2.days()` look totally normal to me.

> `strongman.open( door )` is just that much clearer, flexible and explicit.

Where the strongman came from? Entirely different semantics.


It's implied. Here is the full syntax.

> let time: Timestamp = "2024-07-11T01:14:00Z".parse::<Timestamp>()?;


That ::<TYPE> thing at the end is called a turbofish. It is rarely necessary to give it explicitly (but sometimes you do when the compiler cannot infer the return type on its own—thusfar in my own rust coding I’ve needed it exactly once).


It’s useful to know the full syntax, I’ve definitely encountered needing it more than one time.



I agree. Personally, I'd prefer

    let span = 5.days() + 8.hours() + 1.minutes();



That isn't an implementation of addition between Spans and other Spans. It looks like there isn't one in the library right now. `impl<'a> Add<Span> for &'a Zoned` means a borrow of Zoned is on the left hand side, and a Span on the right. So it says that if z is a Zoned (not a Span) and s is a Span, you can do `&z + s` to add a span to a Zoned. There are a bunch of implementations there, DateTime + Span, Date + Span, Time + Span, Offset + Span. All with Span on the right, but none for Span + Span (nor Span + &Span, or &Span + &Span, ...).


This is correct. You can't do a `span1 + span2`. You'd have to use `span1.checked_add(span2)`. The main problem I had with overloading `+` for span addition is that, in order to add two spans with non-uniform units (like years and months), you need a relative datetime. So `+` would effectively have to panic if you did `1.year() + 2.months()`, which seems like a horrific footgun.

It would be plausible to make `+` for spans do _only_ component wise addition, but this would be an extremely subtle distinction between `+` and `Span::checked_add`. To the point where sometimes `+` and `checked_add` would agree on the results and sometimes they wouldn't. I think that would also be bad.

So I started conservative for the time being: no `+` for adding spans together.


What's the issue with having a span represent "one year and two months"? Both involve a variable number of days (365-366 and 28-31), but I would hope you could store the components separately so it can be unambiguously applied once given a specific moment in time.

I'm thinking something along the lines of how ActiveSupport::Duration works:

    >> 4.years + 5.months + 3.weeks + 2.days + 1.hour + 4.minutes + 10.seconds
    => 4 years, 5 months, 3 weeks, 2 days, 1 hour, 4 minutes, and 10 seconds
Of course the downside being that it would need to be sized large enough to contain each component, even though they may be rarely used.


The components are stored separately. I think what you are advocating for is component wise addition, which I mentioned in my previous comment. It can totally "work," but as I said, it will produce different results than `span1.checked_add(span2)`. For example:

    use jiff::{ToSpan, Unit, Zoned};

    fn main() -> anyhow::Result<()> {
        let span1 = 1.year().months(3);
        let span2 = 11.months();
        let now = Zoned::now().round(Unit::Minute)?;
        let added = span1.checked_add((span2, &now))?;
        println!("{added}");

        Ok(())
    }
Has this output:

    $ cargo -q r
    P2y2m
Notice how the months overflow automatically into years. Days will do the same into months. You need a reference point to do this correctly. For example, just `span1.checked_add(span2)?` would produce an error.

In contrast, component wise addition would lead to a span of `1 year 14 months`. Which is a valid `Span`. Jiff is fine with it. But it's different than what `checked_add` does. Having both operations seems too subtle.

Also, I don't really think using the `+` operator just to construct spans is that much of a win over what Jiff already has. So that needs to be taken into account as well.


The Into<SpanArithmetic> argument feels a little too magical. Since you are not constrained by a trait, I would prefer something like

    Span::add_with_reference(&self, other: &Span, reference: impl Into<SpanRelativeTo>)


I wonder if OP will accept a PR for such a change. Your proposal is much readable and flexible (it's not clear from the docs if you can add random time ranges together). Plus, you'll be able to create your own ranges like `1.decade` or `1.application_timeframe` and add/subtract them.


Have you checked the API to see if that works? I imagine it does.


I like your version's consistency.

The original looks like something Ruby would do.


Only in your memory, I think.

> let span = 5.days().hours(8).minutes(1); # "original"

Actual ruby (with Rails extensions; stock ruby doesn't do this)

    irb(main):001> now = Time.now
    => 2024-07-23 08:31:53.656455305 -0400
    irb(main):002> now + 4.days + 8.minutes + 2.hours
    => 2024-07-27 10:39:53.656455305 -0400
So the version of his that you like for "consistency" is far more closely aligned to what ruby/rails _actually_ does.


I agree with that. In my thought process is to specify what I’m doing and only then some details. This is the other way around. When reading the code, it would be better to see that I’m dealing with span at first.


Or even better:

    let span = Span::days(5).hours(8).minutes(1);


I believe this could actually not work. You would need a `fn days(n: i64) -> Span` and a `fn days(self, n: i64) -> Span` to exist at the same time, which Rust does not allow.


Yeah, or there could simply be a `days()` free function (and equivalents of the other methods too). No need for struct constructors to be associated functions.


I haven't tried it (so I'm sorry if it's wrong or not what you're talking about) but can't you get a freestanding days function by

    use jiff::ToSpan::days;


You cannot import trait methods as free standing functions. I'm not sure if there was a discussion about making this a possibility but it's definitely not something you can do today.


multiple discussions happened for this and I don't quite remember the outcome.

But it's much less simple then it seems.

Because `use Trait::method` would not be one (potential generic) method but a group of them so it would be it's own kind of thing working differently to free functions etc. Furthermore as generics might be on the trait you might not be able to fill them in with `::<>` and even if you fill them in you also wouldn't be able to get a function pointer without having a way to also specify the type the trait is implemented on.

All of this (and probably more issues) are solvable AFIK but in context of this being a minor UX benefit its IMHO not worth it, 1. due to additional compiler complexity but also due to 2. additional language complexity. Through maybe it will happen if someone really cares about it.

Anyway until then you can always define a free function which just calls the method, e.g. `fn default<T: Default>() -> T { T::default() }`. (Which is probably roughly how `use` on a trait method would work if it where a thing.)


Ignoring the technical and UX complexities the desire is somewhat obvious. Particularly for things like ::default().


Oh, sorry about that then.


Can you do 5.days() + 8.hours() + 1.minutes()?


Or 0.days(5).hours(8).minutes(1)?


Great, that gives us 0-days.


If only Rust had named function parameters, you could write what is IMHO the most readable option:

    Span::new(days=5, hours=8, minutes=1)


Yes, that could've been lended almost as-is from OCaml, in particular as Rust doesn't have partial application so optional arguments would work out-of-the-box as well.


Are named arguments on the roadmap somewhere? Or is it a won't-fix?


The feature is controversial enough that it's basically a wontfix.


Is it really, though? I didn't read all the comments, but the ones I read, very few were opposing the concept as such: https://internals.rust-lang.org/t/pre-rfc-named-arguments/16...

However, many were opposing overloading the same pre-rfc was suggesting.


https://github.com/rust-lang/rfcs/issues/323 is the oldest currently open tracking issue, as you can see it has 397 comments. And that thread you linked has 171. Basically, tons of people that feel basically every possible way.

I also feel at this point it's basically a wontfix, but that's more because it's both controversial and huge, and arguably not as high of a priority as other things even if it weren't those two things. The reason it gets huge is that you really want to consider all three of {optional, named, variadric} at roughly the same time.

I do think that people are generally more supportive of variadric than optional/named.


Variadic*


Thanks!


I'm guessing that general support doesn't translate to support for a specific syntax with changes to the calling convention. I wouldn't put money on this coming together any time soon.


I haven't seen anything for or against in actual roadmaps. But there is, of course, at least one pre-proposal:

https://internals.rust-lang.org/t/pre-rfc-named-arguments/16...

It doesn't think about optional arguments (but somehow does include overloading). And a bit in a related fashion, it doesn't permit for reordering calling arguments, which I consider a downside:

> Reordering named arguments when calling

> No it is not possible. Just like unnamed arguments and generics, named arguments are also position-based and cannot be reordered when calling: register(name:surname:) cannot be called as register(surname:name:).

> Reordering them at the definition site is an API break, just like reordering unnamed arguments or generics is an API break already.

The rationale for this expressed in the comments says it's incompatible with overloading, but to me I don't see why named arguments and overloading should go hand-in-hand—or, indeed, how desirable overloading is in the first place. Or why should overloading be able to overload that kind of scenario. The other reasons for this don't seem really problems at all.

    > fn func2(pub name: u32, name hidden: u32) { /\* ... */ }
    > fn func3(name hidden1: u32, name hidden2: u32) { /* ... */ }
> func2 and func3 could work in theory: named arguments as proposed in this RFC are position-based and their internal names are different: just like two arguments can have the same type without ambiguity, those functions could be allowed.

Maybe there are technical reasons that are simpler when considering type compatibility between function types that have or don't have labeled arguments? Seems the proposal has misunderstood something about OCaml labeled arguments when placing it under https://internals.rust-lang.org/t/pre-rfc-named-arguments/16... , though.

In addition the proposal doesn't seem to have a neat syntax for forwarding named parameters, like in constructing records you can just fill in a field called foo by mentioning its name by itself—or, like in OCaml you can have

    let foo ~bar = bar + 1
    let baz ~bar = foo ~bar

    let main () = baz ~bar:42
If it used the .-prefix as mentioned as an idea elsewhere, then this too could be naturally expressed.

Maybe there are other ideas how to go about the labeled arguments, though that one seems pretty well thought-out.

One thing I've enjoyed with Python (and Mypy) is the ability to require the caller to use named arguments with the asterisk marker in the parameter list. This idea is mentioned in the proposal.


I'm all for named parameters. C++ is sorely lacking that feature as well.

Currently using vs code with C++, I like how it handles the missing language feature by adding a grayed out parameter name before the value for function calls and initializers. Maybe there is something like that for rust.


These are called "inlay hints" and exist for most editors/languages.


Yes, editors can be configured to do the same thing for rust.


could you do that with `struct` / `record` fields? In JavaScript which doesn't have named function parameters either I often write functions with a single `cfg` parameter that are called like `f({ hours: 2, seconds: 53, })` which I find nice b/c it re-uses existing data structures.


In Rust, you can't implicitly omit fields when instantiating a struct, so it would have to be a bit more verbose, explicitly using Rust's analog to the spread syntax.

It would have to look something like:

  f({ hours: 2, seconds: 53, ..Default::default() })

The defaults could come from some value / function with a name shorter than Default::default(), but it would be less clear.


Adding support for struct default field values would allow for

- leaving some mandatory fields

- reduce the need for the builder pattern

- enable the above to be written as f(S { hours: 2, seconds: 53, .. })

If that feature ever lands, coupled with structural/anonymous structs or struct literal inference, you're getting everything you'd want from named arguments without any of the foot guns.


Has anyone ever proposed it? It's such a straightforward feature with such obvious semantics ("default field values are const contexts") and impossible-to-bikeshed syntax (`a: i32 = 42`) that I've been meaning to write up an RFC myself for around, oh, ten years now...


nrc was poking at the problem in 2016 https://internals.rust-lang.org/t/struct-field-defaults/3412 which led to this RFC https://github.com/rust-lang/rfcs/pull/1806

It got postponed because it wasn't going to make it into Rust 2018: https://github.com/rust-lang/rfcs/pull/1806#issuecomment-327...


Last time I had this conversation[1] the main sticking point were around semantics of private fields, but I am convinced that the maximally restrictive version can be done (only pub fields can be optional) and maybe relaxed later after we get some real world experience. The other thing was about whether the default values needed to be const, but that makes sense as a restriction to me.

1: https://internals.rust-lang.org/t/pre-pre-rfc-syntactic-suga...


Yeah makes sense! I haven’t felt the need for this feature personally, but it feels like it fits with the language pretty well to me. The big issue with default values (as far as I’m concerned) is for them to always have a default value, and this doesn’t change the opt-in nature of the concept.


The issue I have with Default is that the verbosity jump from derive(Default) to impl Default for Type is high, evej if you just want to specify a single field. The other reason I want this is because it makes defining builders almost trivial for any case where custom logic isn't needed for the fields. I would expect the impl-based builder pattern to become more niche if we had this feature. Your reason is yet another that would be nice to address as well. I like how this one change addresses multiple things at once.


Yeah, full agreement here. I almost said something along the lines of "it's almost like extending #[derive(Default)] with serde-like attributes to control which fields get what defaults, but with nicer syntax," but got a little lazy with it since we're in agreement anyway. It is annoying that deriving Default is all or nothing.

I didn't even think about the builder thing, but you're right there too.


Well the beautiful thing about software engineering is that pretty much everything is possible, it essentially boils down to "but should you really"? :-)


Kind of inefficient. Also it's less ergonomic since every struct is it's own type, so you need to have the signature on both sides.


It should compile to the same because a struct passed by value is loaded into the registers the same as method arguments.




Consider applying for YC's Spring batch! Applications are open till Feb 11.

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: