Skip to content
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

Implement doesNotUnderstandWithKeys #6227

Closed
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
318f139
First commit.
Mar 2, 2024
591e8f6
Format
Mar 2, 2024
b30607d
Format again
Mar 2, 2024
4e77318
Format again again
Mar 2, 2024
b336225
Provide better interface for redirecting function calls
Mar 3, 2024
389c8d3
Comment and refactor code
Mar 4, 2024
958af68
Documentation and small renaming of arg
Mar 4, 2024
be80f55
Remove helper methods from object, and delegate to Object, Function, …
Mar 9, 2024
ec776bc
Update Tuning*doesNotUnderstandWithKeys
Mar 9, 2024
a8fafc1
Renaming and formatting.
Mar 10, 2024
2154969
Remove evaluateWith* methods, simplify the argument creation, update …
Mar 11, 2024
faf6b29
Formatting.
Mar 11, 2024
6d6a86f
Simplify how callable arguments are made.
Mar 12, 2024
d9f07f2
Implement performWith and valueWith
Mar 13, 2024
750f8f5
Remove reformatting.
Mar 13, 2024
61b2c68
Remove reformatting.
Mar 13, 2024
4144747
Move the creation of the argument array to FunctionDef so it can be u…
Mar 13, 2024
32c3333
Update Scale and Tuning
Mar 13, 2024
919956f
Given Object a default value with and fix commit syn issue.
Mar 13, 2024
9dc1da5
Remove VarArgs on performWith
Mar 13, 2024
26428b3
Missed bracket from commit
Mar 13, 2024
aa519f4
Simply Scale's doesNotUnderstandWithKeys
Mar 13, 2024
ab00109
Rename defaultArguments to defaultArgs
Mar 13, 2024
ad04f69
Docs and a comment
Mar 13, 2024
aeb6a95
Docs remove old method
Mar 13, 2024
b3f7a7f
Simply some logic in makePerformableArray
Mar 13, 2024
ebf5805
Actually undo mistaken formatting. Fixed comments. Simplify code.
Mar 14, 2024
f79e656
Depreciate functionPerformList in favour of functionPerformWith
Mar 14, 2024
27679c5
Docs
Mar 14, 2024
704d0cf
Remove indents
Mar 14, 2024
d15afc8
Comment grammar
Mar 14, 2024
76b7708
Reinstate functionPerformList
Mar 14, 2024
024eac1
Reinstate functionPerformList
Mar 14, 2024
0b15f0f
Reinstate functionPerformList
Mar 14, 2024
b38cb22
Reinstate functionPerformList
Mar 14, 2024
ad8f2cb
Formatting, small optimization.
Mar 14, 2024
629686c
Remove default args of valueWith
Mar 17, 2024
d0b8df2
Tidy, rename, lots of docs.
Mar 20, 2024
7826824
Docs parens, undo sc formatting.
Mar 20, 2024
ef4fdc3
Remove semi-colons, Docs typo
Mar 21, 2024
bf55260
Typo and spaces/tabs
Apr 15, 2024
94a6cd2
Typo and spaces/tabs
Apr 15, 2024
5e4a7f8
Undo auto indent.
Apr 15, 2024
eda146e
Remove DoesNotUnderstandWithKeysError
Apr 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 0 additions & 1 deletion HelpSource/Classes/Class.schelp
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,6 @@ method::asString

Return the name of the class as a String.


subsection:: Accessing

method::name
Expand Down
14 changes: 14 additions & 0 deletions HelpSource/Classes/FunctionDef.schelp
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,20 @@ code::
{ |a = 9, b = 10, c| a + b }.def.makeEnvirFromArgs;
::


method:: makePerformableEnvir
Create an link::Classes/IdentityDictionary:: from the passed in arguments and keyword arguments,
ensuring all the arguments will be accepted by the link::Classes/FunctionDef:: and none of the arguments collide with each other.

argument::argsArray
The non-keyword arguments as an link::Classes/Array::.

argument::keywordArgsEvent
The 'keyworded' arguments as an link::Classes/IdentityDictionary::, where the keyword is the key.

method::makePerformableArray
Like link::#-makeNonOverlappingKeywordArgsEvent:: but returns an link::Classes/Array:: with the arguments in the correct order.

subsection::Generating Strings

method::makeFuncModifierString
Expand Down
8 changes: 8 additions & 0 deletions HelpSource/Classes/Method.schelp
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,11 @@ method:: filenameSymbol
returns::
A link::Classes/Symbol:: which is the full path of the source file that this method is defined in.

method::isClassMethod
returns::
A link::Classes/Boolean:: where true represents if this link::Classes/Method:: is a class method,
and false represents when this is an instance method.
::



47 changes: 46 additions & 1 deletion HelpSource/Classes/Object.schelp
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,12 @@ Set[1, 2, 3].tryPerform(\keysValuesDo, { |key, value| [key, value].postln });
(a: 1, b: 2, c: 3).tryPerform(\keysValuesDo, { |key, value| [key, value].flippityblargh });
::

method::functionPerformList
A method called in object prototyping, see link::Classes/IdentityDictionary#-new::.
By default this will do nothing and return the object.
link::Classes/Function#-functionPerformList:: provides a useful implementation.
Not called in normal use.
JordanHendersonMusic marked this conversation as resolved.
Show resolved Hide resolved

method::superPerform

Like perform, superPerform calls a method, however it calls the method on the superclass.
Expand Down Expand Up @@ -613,7 +619,11 @@ Remove all dependants of the receiver. Any object that has had dependants added

subsection::Error Support

Object implements a number of methods which throw instances of Error. A number of methods (e.g. doesNotUnderstand) are 'private' and do not normally need to be called directly in user code. Others, such as those documented below can be useful for purposes such as object oriented design (e.g. to define an abstract interface which will be implemented in subclasses) and deprecation of methods. The reserved keyword thisMethod can be used to refer to the enclosing method. See also Method and Function (for exception handling).
Object implements a number of methods which throw instances of Error.
A number of methods (e.g. doesNotUnderstand) do not normally need to be called directly in user code.
Others can be useful for purposes such as object oriented design (e.g. to define an abstract interface which will be implemented in subclasses) and deprecation of methods.
The reserved keyword thisMethod can be used to refer to the enclosing method.
See also Method and Function (for exception handling).

method::throw

Expand Down Expand Up @@ -652,6 +662,41 @@ foo {
}
::


method::doesNotUnderstand
Not usually called in user code.
JordanHendersonMusic marked this conversation as resolved.
Show resolved Hide resolved
This is called when an object receives a message it does not understand.
Object's implementation will throw an error, but other classes (link::Classes/IdentityDictionary::) might override this.

argument::selector
The name of the message as a link::Classes/Symbol::.

argument::... args
The arguments passed.

code::
4.someThing(2) // ERROR: Message 'someThing' not understood.
::

method::doesNotUnderstandWithKeys
Not usually called in user code.
JordanHendersonMusic marked this conversation as resolved.
Show resolved Hide resolved
This is called when an object receives a message it does not understand, but there are keyword arguments.
JordanHendersonMusic marked this conversation as resolved.
Show resolved Hide resolved
Object's implementation will throw an error, but other classes (link::Classes/IdentityDictionary::) might override this.

argument::selector
The name of the message as a link::Classes/Symbol::.

argument::argsArray
The arguments passed that did not have keywords attached passed as an link::Classes/Array::
JordanHendersonMusic marked this conversation as resolved.
Show resolved Hide resolved

argument::keywordArrayPair
The arguments passed that did have keywords attached.
These are formatted as an link::Classes/Array:: of key-value pairs link::Reference/Key-Value-Pairs::.

JordanHendersonMusic marked this conversation as resolved.
Show resolved Hide resolved
code::
4.someThing(3, someArgument: 42) // ERROR: Message 'someThing' not understood.
::

subsection::Printing and Introspection

method::post
Expand Down
59 changes: 43 additions & 16 deletions SCClassLibrary/Common/Collections/Dictionary.sc
Original file line number Diff line number Diff line change
Expand Up @@ -331,10 +331,10 @@ Dictionary : Set {
var oldElements = array;
array = Array.newClear(array.size * 2);
this.keysValuesArrayDo(oldElements,
{ arg key, val;
index = this.scanFor(key);
array.put(index, key);
array.put(index+1, val);
{ arg key, val;
index = this.scanFor(key);
array.put(index, key);
array.put(index+1, val);
});
}
fixCollisionsFrom { arg index;
Expand Down Expand Up @@ -438,9 +438,9 @@ IdentityDictionary : Dictionary {
index = this.scanFor(key);
array.put(index+1, value);
if ( array.at(index).isNil, {
array.put(index, key);
size = size + 1;
if (array.size < (size * 4), { this.grow });
array.put(index, key);
JordanHendersonMusic marked this conversation as resolved.
Show resolved Hide resolved
size = size + 1;
if (array.size < (size * 4), { this.grow });
});
*/
}
Expand All @@ -453,9 +453,9 @@ IdentityDictionary : Dictionary {
prev = array.at(index + 1);
array.put(index+1, value);
if ( array.at(index).isNil, {
array.put(index, key);
size = size + 1;
if (array.size < (size * 4), { this.grow });
array.put(index, key);
size = size + 1;
if (array.size < (size * 4), { this.grow });
});
^prev
*/
Expand Down Expand Up @@ -560,12 +560,39 @@ IdentityDictionary : Dictionary {
^this.superPerformList(\doesNotUnderstand, selector, args);
}

// Quant support.
// The Quant class assumes the quant/phase/offset scheduling model.
// If you want a different model, you can write a dictionary like so:
// (nextTimeOnGrid: { |self, clock| ... calculate absolute beat number here ... },
// parameter: value, parameter: value, etc.)
// If you leave out the nextTimeOnGrid function, fallback to quant/phase/offset.
doesNotUnderstandWithKeys {|selector, argsArray, keywordArgsAsPairs|
if(know.not){
^this.superPerformList(\doesNotUnderstandWithKeys, selector, argsArray, keywordArgsAsPairs )
};

this[selector] !? {|f|
^f.functionPerformList(
\value,
f.def.makePerformableArray([this] ++ argsArray, keywordArgsAsPairs.asEvent)
)
};

JordanHendersonMusic marked this conversation as resolved.
Show resolved Hide resolved
// Unlike in doesNotUnderstand, we don't convert to a setter as this isn't
// possible with keyword args.

// If the keyword arg names don't match this will fail.
// Note how the selector is passed in here.
this[\forward] !? {|f|
^f.functionPerformList(
Copy link
Member

@telephon telephon Mar 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can then now be:

^f.valueWith([this, selector] ++ argsArray, keywordArgsAsPairs)

right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, that would change how object prototyping calls methods and potentially break stuff.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah sorry, sure. I meant ^func.performWith(\value, [this] ++ argsArray, keywordArgsAsPairs.asEvent)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No that still won't work because it needs to go through functionPerformList which is how classes currently opt into object prototyping. I think it would be better if that was not the case, but that's a different conversation.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought that it may be inefficient to calculate the array when none is needed. So a method like functionPerformWith would be logical, which would simply return this for objects and call valueWith for functions.

But, I suppose it is a rare case anyhow that someone passes arguments when simply accessing an object in a prototype.

Like:

p = (finch: [1, 2, 3]);
p.finch; // yes
p.finch(freq: 900); // rather not

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's not a keyword argument, and should already work? Away from computer, but I'll test later.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p.finch(freq: 900);

Is this to demonstrate a case where the user just wants to access the value?

What you've written throws an error because the keyword does not exist in array's value method (I think...)

I don't think it ever makes sense to use a keyword when you want to access something.

Copy link
Member

@telephon telephon Mar 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I imagine the case where the message finch would be sent to different prototype objects (dictionaries), and some of them have a function in finch to generate a new object, taking freq as an argument, and others have a fixed object in them, ignoring the argument.

In the case of normal objects, this just goes through without complaint:

p = Point(0, 3);
p.x(7)
p.x(where:7)

But my comment was mainly about optimization: if you call (x:600).x(where:7) and there is an object (here 600) is fixed, there is no need to go through the whole makePerformableArray.

But this is just something to keep in mind, I suppose.

Essentially a method functionPerformWith would solve it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But that syntax doesn't cause any conflict, does it?

it should work as usual. I was just referring to the getter (not the setter), a call to which may have more arguments that remain unused.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I think this bit of the code is wrong because right now it only works with functions, and won't work with other classes as it just looks up the def. I'll change this now.

\value,
f.def.makePerformableArray([this, selector] ++ argsArray, keywordArgsAsPairs.asEvent)
)
};

^nil
}

// Quant support.
// The Quant class assumes the quant/phase/offset scheduling model.
// If you want a different model, you can write a dictionary like so:
// (nextTimeOnGrid: { |self, clock| ... calculate absolute beat number here ... },
// parameter: value, parameter: value, etc.)
// If you leave out the nextTimeOnGrid function, fallback to quant/phase/offset.

nextTimeOnGrid { |clock, referenceBeat|
if(this[\nextTimeOnGrid].notNil) {
Expand Down
33 changes: 25 additions & 8 deletions SCClassLibrary/Common/Collections/Scale.sc
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,16 @@ Scale {
}

*doesNotUnderstand { |selector, args|
var scale = this.newFromKey(selector, args).deepCopy;
^scale ?? { super.doesNotUnderstand(selector, args) };
^this.newFromKey(selector, args).deepCopy
JordanHendersonMusic marked this conversation as resolved.
Show resolved Hide resolved
!? { |v| v } //return if valid
?? { super.doesNotUnderstand(selector, args) }
}
*doesNotUnderstandWithKeys {|selector, argsArray, keywordArgsAsPairs|
^Scale.performWithEnvir(
\newFromKey,
Scale.class.findRespondingMethodFor(\newFromKey)
.makePerformableEnvir([selector] ++ argsArray, keywordArgsAsPairs)
)
}
JordanHendersonMusic marked this conversation as resolved.
Show resolved Hide resolved

*newFromKey { |key, tuning|
Expand Down Expand Up @@ -185,8 +193,17 @@ Tuning {
}

*doesNotUnderstand { |selector, args|
var tuning = this.newFromKey(selector, args).deepCopy;
^tuning ?? { super.doesNotUnderstand(selector, args) }
^this.newFromKey(selector, args).deepCopy
JordanHendersonMusic marked this conversation as resolved.
Show resolved Hide resolved
!? { |v| v } //return if valid
?? { super.doesNotUnderstand(selector, args) }
}

*doesNotUnderstandWithKeys {|selector, argsArray, keywordArgsAsPairs|
JordanHendersonMusic marked this conversation as resolved.
Show resolved Hide resolved
^Tuning.performWithEnvir(
\newFromKey,
Tuning.class.findRespondingMethodFor(\newFromKey)
.makePerformableEnvir([selector] ++ argsArray, keywordArgsAsPairs)
)
JordanHendersonMusic marked this conversation as resolved.
Show resolved Hide resolved
}

*newFromKey { | key |
Expand Down Expand Up @@ -295,7 +312,7 @@ ScaleAD : Scale {
var <>descScale;
*new { | degrees, pitchesPerOctave, descDegrees, tuning, name = "Unknown Scale" |
^super.new(degrees, pitchesPerOctave, tuning, name)
.descScale_(Scale(descDegrees, pitchesPerOctave, tuning, name ++ "desc"))
.descScale_(Scale(descDegrees, pitchesPerOctave, tuning, name ++ "desc"))
;
}
asStream { ^ScaleStream(this, 0) }
Expand Down Expand Up @@ -554,7 +571,7 @@ ScaleStream {
8.94, 10.04, 10.9], 2, "Meantone, 1/6 Pythagorean Comma"),
\kirnberger -> Tuning.new([1, 256/243, (5.sqrt)/2, 32/27, 5/4, 4/3,
45/32, 5 ** 0.25, 128/81, (5 ** 0.75)/2, 16/9, 15/8].ratiomidi, 2,
"Kirnberger III"),
"Kirnberger III"),
\werckmeister -> Tuning.new(#[0, 0.92, 1.93, 2.94, 3.915, 4.98, 5.9, 6.965,
7.93, 8.895, 9.96, 10.935], 2, "Werckmeister III"),
\vallotti -> Tuning.new(#[0, 0.94135, 1.9609, 2.98045, 3.92180, 5.01955,
Expand Down Expand Up @@ -588,13 +605,13 @@ ScaleStream {
8/7, 7/6, 32/27, 6/5, 11/9, 5/4, 14/11, 9/7, 21/16, 4/3, 27/20, 11/8,
7/5, 10/7, 16/11, 40/27, 3/2, 32/21, 14/9, 11/7, 8/5, 18/11, 5/3, 27/16,
12/7, 7/4, 16/9, 9/5, 20/11, 11/6, 15/8, 40/21, 64/33, 160/81].ratiomidi, 2,
"Harry Partch"),
"Harry Partch"),
\catler -> Tuning.new([1, 33/32, 16/15, 9/8, 8/7, 7/6, 6/5, 128/105, 16/13,
5/4, 21/16, 4/3, 11/8, 45/32, 16/11, 3/2, 8/5, 13/8, 5/3, 27/16, 7/4,
16/9, 24/13, 15/8].ratiomidi, 2, "Jon Catler"),
\chalmers -> Tuning.new([1, 21/20, 16/15, 9/8, 7/6, 6/5, 5/4, 21/16, 4/3, 7/5,
35/24, 3/2, 63/40, 8/5, 5/3, 7/4, 9/5, 28/15, 63/32].ratiomidi, 2,
"John Chalmers"),
"John Chalmers"),
\harrison -> Tuning.new([1, 16/15, 10/9, 8/7, 7/6, 6/5, 5/4, 4/3, 17/12, 3/2,
8/5, 5/3, 12/7, 7/4, 9/5, 15/8].ratiomidi, 2, "Lou Harrison"),
\sruti -> Tuning.new([1, 256/243, 16/15, 10/9, 9/8, 32/27, 6/5, 5/4, 81/64,
Expand Down
50 changes: 50 additions & 0 deletions SCClassLibrary/Common/Core/Error.sc
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,56 @@ DoesNotUnderstandError : MethodError {
}
}

DoesNotUnderstandWithKeysError : MethodError {
var <>selector, <>argsWithOutKeys, <>argsWithKeys, <suggestedCorrection, suggestion = "";
*new { arg receiver, selector, argsWithOutKeys, argsWithKeys;
^super.new(nil, receiver)
JordanHendersonMusic marked this conversation as resolved.
Show resolved Hide resolved
.selector_(selector)
.argsWithOutKeys_(argsWithOutKeys)
.argsWithKeys_(argsWithKeys.asEvent)
.init
}

init {
var methods, methodNames, editDistances, minIndex, lowerCaseSelector;

if(receiver.notNil) {
methods = receiver.class.superclasses.add(receiver.class).collect(_.methods).reduce('++');
methodNames = methods.collect { |x| x.name.asString.toLower };
// we compare lower-case versions to prioritize capitalization mistakes
lowerCaseSelector = selector.asString.toLower;
editDistances = methodNames.collect(_.editDistance(lowerCaseSelector));
minIndex = editDistances.minIndex;
// Edit distance of 3 chosen arbitrarily; also, filter out completely dissimilar matches
// to avoid unhelpful suggestions.
if(editDistances[minIndex] <= 3 and: { methodNames[minIndex].similarity(lowerCaseSelector) > 0 }) {
suggestedCorrection = methods[minIndex];
suggestion = "\nPerhaps you misspelled '%', or meant to call '%' on another receiver?"
.format(suggestedCorrection.name, selector);
}
}
}
errorString {
^"ERROR: Message '" ++ selector ++ "' not understood." ++ suggestion
}
reportError {
this.errorString.postln;
"RECEIVER:\n".post;
receiver.dump;
"ARGS WITHOUT KEYS:\n".post;
argsWithOutKeys.dumpAll;
"ARGS WITH KEYS:\n".post;
argsWithKeys.dumpAll;
this.errorPathString.post;
if(protectedBacktrace.notNil, { this.postProtectedBacktrace });
this.dumpBackTrace;
// this.adviceLink.postln;
JordanHendersonMusic marked this conversation as resolved.
Show resolved Hide resolved
"^^ %\nRECEIVER: %\n\n\n".postf(this.errorString, receiver);
}
adviceLinkPage {
^"%#%".format(this.class.name, selector)
}
}

MustBeBooleanError : MethodError {
errorString {
Expand Down