-
-
Notifications
You must be signed in to change notification settings - Fork 933
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
Function scopes #1032
Function scopes #1032
Conversation
I am against “function scopes”, at least at the level that I understand them. I still think there’s a chance I’m misunderstanding something. No other standard library I have ever seen does it this way. Every unusual thing we do is another weird thing a programmer has to learn, and if there are enough of these weird things the language feels clunky to use and learn. The best way to make a language easy to pick up is to lean into the patterns programmers already know, not add special cases for convenience. Having some sense of internal logical consistency makes it easier for programmers to figure out what is valid and what is not. Let’s look at some examples. For the The initial example of I hope I don’t come off as rude here, especially considering the significant amount of very-much-appreciated work here that @PgBiel has put into this, but I do not think this would be a positive change for the langauge. |
I think some fundamental misconception is at work here. Function scopes work like static methods in classes - it's just that, in Typst, "classes" are elements, which boil down to functions (e.g.
Relating to what I said above, this change is focused on elements, but extended to functions since both are very similar things in Typst. Now, note that this is consistent with the "Typst" way of doing things - instead of having tons of variables to indicate different things like in LaTeX (e.g.
I actually think it's fair to demand to be able to do this from the typst-side, but this initial PR focuses on making the feature possible in the first place, so that other PRs may build on top of this. Worth saying that this approach also has the advantage of being able to import all functions related to another within a scope, without much hassle. For example, if you're working with canvas, instead of having tons of functions like #canvas[
#import canvas: *
#draw((1, 2), (3, 4))
#node("a")
#draw((5, 6), (7, 8))
// ...
] Sure, you could have a
I think there was some miscommunication here - it's not any more stateful than it would be if it were called
As said above, this wouldn't do well for documentation, as the sub-functions wouldn't be in any namespace.
I appreciate the feedback! But I think more consideration was needed regarding the language as a whole. This PR actually doesn't introduce any "new" syntax (as I comment above), and brings something that I believe is more intuitive for the users, overall. But we can discuss this further and add any needed improvements. |
For one, I think it is uncharacteristically lazy to reject the obvious solution in favor of something so much less desirable just because the documentation generator isn’t quite good enough yet. |
Okay, you do have a point - that by itself isn't enough to justify this addition. I think it boils down to a design choice in the end, just like all things in a language. But, at least for me, it makes sense for elements to have their own "sub-elements" here. At least this is the most valid use-case in my opinion - arguments can be held over the usage of scopes on more trivial things like To be clear, though - your concerns, by themselves, are valid, and by no means do I wish to diminish them. What's important to point out here is that, if you had those concerns, then new users can too. So I think we should make things clear since day-one - an explanation for this should be at least in the tutorial, if this is indeed added. The main thing here is that, while I'd love to be able to implement scopes for user-defined functions in this PR, it faces the future-proofing issue: currently, functions have the In the end, though, I just want to avoid having this discussion devolve into some sort of "fear of change" - if we all got used to new math syntax, then I think we can get used to this too (as such syntax even exists already anyway)... so the main focus of the discussion should be whether or not this feature can achieve its goal, of bringing some intuitive calls and also allowing easier usage of related functions in some contexts (such as |
Forgot to mention, but I also think calling it "lazy" is a bit exaggerated, no? Just making a context dict and writing the docs manually is much simpler than any of this... this is just a different solution, you know? And I thought it looked good, and several others did too, so I worked on it. Idk. Is it really that bad? 🤔 Like, I can't see how it would hinder language usability in any shape or form. I think that being able to keep auxiliary functions/"classes" inside their parents enables us to make public a lot more of the internals without polluting the global namespace. This includes the examples I mentioned ( |
This was just an additional consideration. The main point is that I don't think that it's less desirable. The function scopes are more flexible than the closure callback (for instance, in the closure you can't mutate variables from outside, which you might want). And aside from the drawing thing, it generalizes well to other parts of the language and is consistent with symbol notation. But @PgBiel argued all of this already much better than I would've done. |
Yes! That is something I forgot to mention, but it's very very important - closures are less flexible than simple blocks. |
library/src/compute/foundations.rs
Outdated
#[default] | ||
message: Option<EcoString>, | ||
) -> Value { | ||
if !typst::eval::ops::equal(&expected, &actual) { |
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.
!typst::eval::ops::equal(&expected, &actual)
is the same as expected != actual
, see here
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.
Oooh, I missed that, thanks.
library/src/compute/foundations.rs
Outdated
/// Returns: | ||
#[func] | ||
pub fn assert_eq( | ||
/// The expected value. |
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.
I wouldn't call one "expected" and one "actual". I, for one, more frequently write them in the other order in Rust. Rust just calls them left and right. This of course also affects the error message.
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.
Alright! Is the new message fine?
library/src/compute/foundations.rs
Outdated
/// The actual value. | ||
actual: Value, | ||
|
||
/// An optional message to display on error |
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.
all comments should wrap at 80 columns
library/src/layout/enum.rs
Outdated
/// customize the number of each item in the enumeration: | ||
/// ```example | ||
/// #enum( | ||
/// enum.item(1)[First step], |
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.
2 spaces of indent :)
also in other places.
src/eval/mod.rs
Outdated
@@ -1077,7 +1086,14 @@ impl Eval for ast::FuncCall { | |||
} else { | |||
let target = target.eval(vm)?; | |||
let args = args.eval(vm)?; | |||
if !matches!(target, Value::Symbol(_) | Value::Module(_)) { | |||
|
|||
// Prioritize a function's own methods (with, where) over its fields. |
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.
no need to duplicate the comment, bad enough that the whole code is a bit duplicate here (not your fault)
src/eval/func.rs
Outdated
) | ||
}), | ||
Repr::Closure(_) => Err(eco_format!( | ||
"cannot access fields on closures and user-defined functions" |
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.
"closure" isn't a term that is exposed to the user.
"cannot access fields on closures and user-defined functions" | |
"cannot access fields on user-defined functions" |
src/eval/mod.rs
Outdated
} | ||
if let Value::Func(func) = source { | ||
if func.info().is_none() { | ||
bail!(span, "cannot import from closures or user-defined functions"); |
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.
bail!(span, "cannot import from closures or user-defined functions"); | |
bail!(span, "cannot import from user-defined functions"); |
The docs generation will of course need to be updated. I'll take care of that before the release. |
Thanks! |
Closes #843.
Summary of changes
enum.item
as a function under theenum
function, allowing the creation of enum item objects to be able to manipulate their numbers. (Previously, this required using an array(number, body)
.)assert.eq
andassert.ne
as samples, which assert if two values are equal and not equal, respectively. (The function names can be discussed, but that's the idea.)#import
names from function scopes:Implementation details
#[func]
) or element (#[element(...)]
), use the syntax (in "Rust-side"):scope: Scope
field was added toFuncInfo
(shared by both native funcs and element funcs), which holds a function's scope.#[func]
and#[element(...)]
proc macros. By default, it generates aScope::new()
. However, if you give it something like:then it will generate:
FieldParser
was renamed toBlockWithReturn
and moved toutil
, as its very implementation was reused for this purpose. (The#[parse]
attribute shares this parsing implementation of blocks with a returning expression.)Func::get
was implemented..with
and.where
, method resolution code ineval/mod.rs
was changed to prioritize a function's methods over its fields when doingfunction.something()
. That is,function.with()
andfunction.where()
will call methods, whilefunction.whatever()
will fetch the field first.For(Undone after review.)assert.eq
andassert.ne
to be able to use==
and!=
, the moduleeval.ops
was madepub
.src/ide/complete.rs
was updated to consider function scopes in autocomplete.Discussion points
#import func: *
will always succeed (whenfunc
isn't a closure), even if the function's scope is empty. (Should it error if it has no scope fields/functions?)#import func
will succeed and import the function's actual name to the scope. So, ifenum
is inside a dictionary'se
key and you do#import dict.e
, theenum
name will be brought to scope. This behavior could be changed as needed.enum.item
,assert.eq
andassert.ne
, I used the syntax (e.g.)[assert.eq]($func/assert.eq)
, but I'm not sure if this is ideal (e.g. could conflict with an option with the same name).assert.eq
andassert.ne
(as opposed to, e.g.assert.eq.not
or something) are debatable.(number, body)
syntax for enum items yet, as that would (I guess) be a breaking change; but please tell me if it's fine to remove it.