-
-
Notifications
You must be signed in to change notification settings - Fork 134
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
Standardise representation of values #60
Comments
related #63 |
I'll try to summarize the problem with hap values.
1. haps with object values
2. haps with primitive values
3. pattern methods that apply
|
Nice summary!
I see merits in all these options, but I quite like the last one, as it's ambiguous and raising an error is maybe the least confusing thing to do. But in that case it would be a bit confusing if
|
(I took the liberty to add letters to your options) I'd go for 8. core repl exampleLet's take this unmodified example from the core repl: sequence(880, [440, 660], 440, 660) // 880, ...
.div(slowcat(3,2).slow(2)) // 880/3, ...
.off(1/8,mul(2))
.off(1/4,mul(3)) Note that the core repl will treat numbers as frequencies by default, so no need for objects. 9. example with aIf we would want to adapt that for freq(sequence(880, [440, 660], 440, 660)) // { freq: 880 }, ...
.div(slowcat(3,2).slow(2)) // { freq: 880*3 }, ...
.off(1/8,mul(2))
.off(1/4,mul(3)) 10.1 example with dFor freq(
sequence(880, [440, 660], 440, 660) // 880, ...
.div(slowcat(3,2).slow(2)) // 880 / 3, ...
.off(1/8,mul(2))
.off(1/4,mul(3))
) // { freq: 880 / 3 }, ... 10.2 example with d, alternative... or wrap arithmetic operations in freq (which is imho more confusing): sequence(880, [440, 660], 440, 660) // 880, ...
.div(freq(slowcat(3,2).slow(2))) // 880/3, ...
.off(1/8,mul(freq(2)))
.off(1/4,mul(freq(3))) // { freq: 880 / 3 }, ... If we don't find a case where 11.1 multi member object operations with aIn some cases, it can even be benefitial to apply the operation on all object members with freq(220) // { freq: 220 }
.cutoff(1000) // { freq: 220, cutoff: 1000 }
.mul(2) // { freq: 440, cutoff: 2000 } <-- cutoff follows frequency!
.s('supersaw') // { freq: 440, cutoff: 2000, s: 'supersaw' } 11.2 multi member objects, single member operationsIf we don't want to apply it to both keys, we just need to change the order: freq(220) // { freq: 220 }
.mul(2) // { freq: 440 }
.cutoff(1000) // { freq: 440, cutoff: 1000 } <-- cutoff is not multiplied
.s('supersaw') // { freq: 440, cutoff: 1000, s: 'supersaw' } 11.3 multi member objects, single member operationsor cutoff(1000)
.mul(2) // { cutoff: 2000 }
.freq(220) // { cutoff: 2000, freq: 220 } // <-- freq is not multiplied
.s('supersaw') // { cutoff: 2000, freq: 220, s: 'supersaw' } 12 multi member objects, single member operationsIf we want to replicate 11.1 with freq(220) // { freq: 220 }
.cutoff(1000) // { freq: 220, cutoff: 1000 }
.mul(freq(cutoff(2))) // { freq: 440, cutoff: 2000 } <-- cutoff follows frequency!
.s('supersaw') // { freq: 440, cutoff: 2000, s: 'supersaw' } or is there another way? Those are just some thoughts to keep the ball rolling..
|
Hm well I think 10.1 is the clearest expression. I don't see the parenthesis introducing confusion, only resolving ambiguity. Parenthesis is all about being clear about scope and execution order, and that's what we're trying to do here. Having to deal with nested parentheses can be confusing, counting and matching brackets.. But that's a bit inevitable with trying to represent tree structures in text, and editor features can help with that. Things are also confusing with the mixing of nouns like freq(220).union(cutoff(1000).mul("2 4")) ... where we don't treat nouns as methods of other nouns. Then if we wanted to multiply both we could do it to the union: freq(220).union(cutoff(1000)).mul("2 4") I guess the main reason for avoiding this is again the difficulty with bracket matching. That might be the best design choice but I think we should take a bit of care to make sure that this really is the best trade-off. For example if we overloaded an operator instead of union we could do something like: freq(220) >> cutoff(1000).mul("2 4") That (freq(220) >> cutoff(1000)).mul("2 4") |
Agreed that it resolves ambiguity. But we should note that 10.1 would still work with
Totally, but in many cases, you don't want a separate tree branch as you only have one object member
Yes it can get a bit confusing mixing those words. So would this snippet be a different approach then, implying that Wouldn't freq(220).union(cutoff("1000".mul("2 4"))) Agreed that operator overloading helps against too much parens. We should still keep in mind that operator overloading will only work with shapeshifting and will not be part of the core api. As I see it atm,
|
Sorry I was assuming freq(220) >> cutoff(1000).mul("2 4") I think Then with freq(220) >> cutoff(1000) * "2 4" With shapeshifting do we have control over precedence in the above? Hm it's a shame that shapeshifting is needed for this. I do worry though that by using |
As an aside, the CDN is useful for mapping out and talking about these decisions: https://en.wikipedia.org/wiki/Cognitive_dimensions_of_notations For example terseness seems like a plus for live coding, but I think viscosity is often more important. |
"." has higher precedence. Just checked with https://astexplorer.net/
Yup. Maybe we'll need that full on DSL someday...
This will also work in the expected order. I think before we talk about operator overloading, we should have an API that is acceptable without, maybe fixing some verbosity with operators. I don't really see the danger of ending up with bead sequences, as we always have the option to explicitly nest stuff if we want.. |
That's good on precedence. Without shapeshifting, we could also use union(
freq(220),
cutoff(2000).mul(2)
) I don't really like the word We're not completely free to nest stuff with method chaining, as this won't work: freq(220).(cutoff(2000).mul(2)) |
Why not just freq(220).cutoff("2000".mul(2)) |
Yes, that's better, but there isn't a good reason why It is also possible to contrive an example where you would want to group two parameters with freq("220(5,8)")
. (cutoff(sine.range(200,400))
. room(saw)
).every(3, fast(2)) |
unfortunately, using dots this way wont work with js. we could still add union for such cases. but i wonder if you even need parens for union, because they say it‘s associative: https://proofwiki.org/wiki/Union_is_Associative At least in your example, dropping the parens won‘t make a difference, but I might totally miss something here |
Ah right, so I'd need more parenthesis.. freq("220(5,8)")
. ((cutoff(sine.range(200,400))
. room(saw)
).every(3, fast(2))) Except that doesn't work, so it'd have to be: freq("220(5,8)")
. (union((cutoff(sine.range(200,400))
. room(saw)
)).every(3, fast(2))) Or with an overloaded operator: freq("220(5,8)")
>> (cutoff(sine.range(200,400))
>> room(saw)
).every(3, fast(2)) Having Also making it seem like var foo = freq(200) . cutoff(4000);
s("bd sd") . foo // couldn't work
s("bd sd") >> foo // could work with shapeshifting
s("bd sd") . union(foo) // works
union(
s("bd sd"),
foo
) // also works Of course what's really happening is that |
Maybe plain javascript is just too restrictive to host a DSL, and we should put shapeshifting in core, or at least make it highly recommended for end-user live coding. |
freq("220(5,8)")
. ((cutoff(sine.range(200,400))
. room(saw)
).every(3, fast(2))) So this example is implying we want the Couldn't the same be achieved much simpler, using: freq('220(5,8)').union(
cutoff(sine.range(200, 400))
.room(saw)
.every(3, fast(2))
); I think this is pretty readable even without an operator. With operator: freq('220(5,8)') >>>
cutoff(sine.range(200, 400))
.room(saw)
.every(3, fast(2))
I don't think this is a leaky abstraction, you could see the dot not as an operator, but more as a syntactical separator of functions (like "," for arguments). freq(220).cutoff("800 1000").fast(2)
!==
freq(220).cutoff("800 1000".fast(2)) On the left, the The same applies for other modifier functions: freq(220).cutoff("800 1000").mul(2)
// { freq: 440, cutoff: 1600 }, ...
!==
freq(220).cutoff("800 1000".mul(2))
// { freq: 220, cutoff: 1600 }, ...
We could ensure freq("220 440").room(".5 1")
=== room(".5 1") .freq("220 440")
=== freq("220 440" .room(".5 1"))
=== room(".5 1" .freq("220 440")) Here, I think "syntax sugar" like that (being able to union without explictly writing .union) won't hurt if there is still an explicit alternative. I guess most strudlers won't think about those language intricacies, and just learn the behaviour in a more implicit way, so any shortcuts are beneficial and make it more accessible. I think use cases that require But maybe there are special cases or further complexities I am missing so far. We could also discuss some more code examples, using different notations. As you've said earlier, this discussion is really about core design choices, so we should pay attention to detail here. |
My argument wasn't that freq("220(5,8)")
. ((cutoff(sine.range(200,400))
. room(saw)
).every(3, fast(2))) is the best way to write the pattern, but that it should work, because It feels better without the space though e.g. It's a shame that something like these won't work: freq(220).every(3, .room(0.5))
freq(220).every(3, >> room(0.5)) Alternatives: freq(220).every(3, x => x.room(0.5))
freq(220).every(3, x => x >> room(0.5))
freq(220).every(3, room(0.5).union) They aren't commutative when they have different structures: freq("220 440").room(".5 1 2") We could make it so they are, by doing the union with (In tidal there are different operators for The |
The problem with this: freq("220 230 240").every(3, room("0.5 0.25").union) Is that event fragments would take the whole timespan from the 'room', and you'd end up with two event onsets instead of three, which might be surprising. Considering #82, the original structure could be maintained with freq("220 230 240").every(3, room("0.5 0.25").setFlip) |
So in the end I agree that methods should try their best to apply to the whole pattern, e.g. choice 'a' where this adds to both n("0 1").room(.5).add(.2) and that we keep Maybe we should resolve #82 next. |
okay so I kind of lost track of the common thread, but I think we should be alright with union approach
happy to hear that.
Yep.. In this case, it would be desirable if So the problem is: we want 3 different behaviours (1. create pattern, 2. assign value to pattern 3. create pattern modifier function), but we only have 2 functions to spare.. But there may be a way out of this: What if every(n, func) {
if (func instanceof Pattern) { // <-- this
const p = func;
func = (x) => x.union(p);
}
const pat = this;
const pats = Array(n - 1).fill(pat);
pats.unshift(func(pat));
return slowcatPrime(...pats);
} I already tested this and it works. Similar tactic could be used for other functions that expect a pattern modifier function. What do you think? |
Yes that looks good, as it also allows this to work: freq(220).every(3, room(0.5).size(0.5)) (With #83, that would be |
Do we agree that velocity should go into the value object? |
clearly it should |
I think this issue is out of date, apart from some parameters |
yep let's close this monster issue.. |
Bring in unused
value.mjs
, and put all data in the value as an object, leaving context for things independent of value, like source code position.The text was updated successfully, but these errors were encountered: