From cbae3558969ec85c003c90a076ffeff6fa254bd4 Mon Sep 17 00:00:00 2001 From: Alex McLean Date: Thu, 16 Feb 2023 23:15:21 +0000 Subject: [PATCH] Another attempt at composable functions - WIP (#390) Summary of changes: - Made unary functions composable, including controls. So e.g. s("bd sd").every(3,fast(2).iter(4).n(4)) works the same as s("bd sd").every(3,x => x.fast(2).iter(4).n(4)) - Made operators/alignments composable too, so s("bd sd").every(3, set.squeeze.n(3, 4)) works - Patterns are not treated as functions, so s("bd sd").every(3, n(5)) is an annoying runtime error. s("bd sd").every(3, set.n(5)) does work though. Other minor changes: - standardised alignment 'squeezeOut' as lowercase 'squeezeout' - made firstCycleValues turn haps sorted in order of 'part' --- packages/core/controls.mjs | 20 +- packages/core/pattern.mjs | 330 ++++++++++++++++++++++------ packages/core/test/pattern.test.mjs | 28 ++- packages/core/util.mjs | 2 +- 4 files changed, 299 insertions(+), 81 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 4e230240..c07045ff 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see . */ -import { Pattern, sequence } from './pattern.mjs'; +import { Pattern, sequence, registerControl } from './pattern.mjs'; const controls = {}; const generic_params = [ @@ -828,26 +828,26 @@ const generic_params = [ // TODO: slice / splice https://www.youtube.com/watch?v=hKhPdO0RKDQ&list=PL2lW1zNIIwj3bDkh-Y3LUGDuRcoUigoDs&index=13 -const _name = (name, ...pats) => sequence(...pats).withValue((x) => ({ [name]: x })); - -const _setter = (func, name) => - function (...pats) { +const makeControl = function (name) { + const func = (...pats) => sequence(...pats).withValue((x) => ({ [name]: x })); + const setter = function (...pats) { if (!pats.length) { return this.fmap((value) => ({ [name]: value })); } return this.set(func(...pats)); }; + Pattern.prototype[name] = setter; + registerControl(name, func); + return func; +}; generic_params.forEach(([type, name, description]) => { - controls[name] = (...pats) => _name(name, ...pats); - Pattern.prototype[name] = _setter(controls[name], name); + controls[name] = makeControl(name); }); // create custom param controls.createParam = (name) => { - const func = (...pats) => _name(name, ...pats); - Pattern.prototype[name] = _setter(func, name); - return (...pats) => _name(name, ...pats); + return makeControl(name); }; controls.createParams = (...names) => diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index b9c9e24c..1184e1a1 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -21,6 +21,141 @@ let stringParser; // intended to use with mini to automatically interpret all strings as mini notation export const setStringParser = (parser) => (stringParser = parser); +const alignments = ['in', 'out', 'mix', 'squeeze', 'squeezeout', 'trig', 'trigzero']; + +const methodRegistry = []; +const getterRegistry = []; +const controlRegistry = []; +const controlSubscribers = []; +const composifiedRegistry = []; + +////////////////////////////////////////////////////////////////////// +// Magic for supporting higher order composition of method chains + +// Dresses the given (unary) function with methods for composition chaining, so e.g. +// `fast(2).iter(4)` composes to pattern functions into a new one. +function composify(func) { + if (!func.__composified) { + for (const [name, method] of methodRegistry) { + func[name] = method; + } + for (const [name, getter] of getterRegistry) { + Object.defineProperty(func, name, getter); + } + func.__composified = true; + composifiedRegistry.push(func); + } else { + console.log('Warning: attempt at composifying a function more than once'); + } + return func; +} + +export function registerMethod(name, addAlignments = false, addControls = false) { + if (addAlignments || addControls) { + // This method needs to make its 'this' object available to chained alignments and/or + // control parameters, so it has to be implemented as a getter + const getter = { + get: function () { + const func = this; + const wrapped = function (...args) { + const composed = (pat) => func(pat)[name](...args); + return composify(composed); + }; + + if (addAlignments) { + for (const alignment of alignments) { + wrapped[alignment] = function (...args) { + const composed = (pat) => func(pat)[name][alignment](...args); + return composify(composed); + }; + for (const [controlname, controlfunc] of controlRegistry) { + wrapped[alignment][controlname] = function (...args) { + const composed = (pat) => func(pat)[name][alignment](controlfunc(...args)); + return composify(composed); + }; + } + } + } + if (addControls) { + for (const [controlname, controlfunc] of controlRegistry) { + wrapped[controlname] = function (...args) { + const composed = (pat) => func(pat)[name](controlfunc(...args)); + return composify(composed); + }; + } + } + return wrapped; + }, + }; + + getterRegistry.push([name, getter]); + + // Add to functions already 'composified' + for (const composified of composifiedRegistry) { + Object.defineProperty(composified, name, getter); + } + } else { + // No chained alignments/controls needed, so we can just add as a plain method, + // probably more efficient this way? + const method = function (...args) { + const func = this; + const composed = (pat) => func(pat)[name](...args); + return composify(composed); + }; + + methodRegistry.push([name, method]); + + // Add to functions already 'composified' + for (const composified of composifiedRegistry) { + composified[name] = method; + } + } +} + +export function registerControl(controlname, controlfunc) { + registerMethod(controlname); + controlRegistry.push([controlname, controlfunc]); + for (const subscriber of controlSubscribers) { + subscriber(controlname, controlfunc); + } +} + +export function withControls(func) { + for (const [controlname, controlfunc] of controlRegistry) { + func(controlname, controlfunc); + } + controlSubscribers.push(func); +} + +export function addToPrototype(name, func) { + Pattern.prototype[name] = func; + registerMethod(name); +} + +export function curryPattern(func, arity = func.length) { + const fn = function curried(...args) { + if (args.length >= arity) { + return func.apply(this, args); + } + + const partial = function (...args2) { + return curried.apply(this, args.concat(args2)); + }; + if (args.length == arity - 1) { + return composify(partial); + } + + return partial; + }; + if (arity == 1) { + composify(fn); + } + return fn; +} + +////////////////////////////////////////////////////////////////////// +// The core Pattern class + /** @class Class representing a pattern. */ export class Pattern { /** @@ -643,7 +778,9 @@ export class Pattern { * @noAutocomplete */ get firstCycleValues() { - return this.firstCycle().map((hap) => hap.value); + return this.sortHapsByPart() + .firstCycle() + .map((hap) => hap.value); } /** @@ -693,7 +830,7 @@ export class Pattern { const otherPat = reify(other); return this.fmap((a) => otherPat.fmap((b) => func(a)(b))).squeezeJoin(); } - _opSqueezeOut(other, func) { + _opSqueezeout(other, func) { const thisPat = this; const otherPat = reify(other); return otherPat.fmap((a) => thisPat.fmap((b) => func(b)(a))).squeezeJoin(); @@ -860,11 +997,11 @@ function groupHapsBy(eq, haps) { const congruent = (a, b) => a.spanEquals(b); // Pattern> -> Pattern> // returned pattern contains arrays of congruent haps -Pattern.prototype.collect = function () { +addToPrototype('collect', function () { return this.withHaps((haps) => groupHapsBy(congruent, haps).map((_haps) => new Hap(_haps[0].whole, _haps[0].part, _haps, {})), ); -}; +}); /** * Selects indices in in stacked notes. @@ -872,12 +1009,12 @@ Pattern.prototype.collect = function () { * note("<[c,eb,g]!2 [c,f,ab] [d,f,ab]>") * .arpWith(haps => haps[2]) * */ -Pattern.prototype.arpWith = function (func) { +addToPrototype('arpWith', function (func) { return this.collect() .fmap((v) => reify(func(v))) .innerJoin() .withHap((h) => new Hap(h.whole, h.part, h.value.value, h.combineContext(h.value))); -}; +}); /** * Selects indices in in stacked notes. @@ -885,9 +1022,9 @@ Pattern.prototype.arpWith = function (func) { * note("<[c,eb,g]!2 [c,f,ab] [d,f,ab]>") * .arp("0 [0,2] 1 [0,2]").slow(2) * */ -Pattern.prototype.arp = function (pat) { +addToPrototype('arp', function (pat) { return this.arpWith((haps) => pat.fmap((i) => haps[i % haps.length])); -}; +}); ////////////////////////////////////////////////////////////////////// // compose matrix functions @@ -985,15 +1122,15 @@ function _composeOp(a, b, func) { func: [(a, b) => b(a)], }; - const hows = ['In', 'Out', 'Mix', 'Squeeze', 'SqueezeOut', 'Trig', 'Trigzero']; + const hows = alignments.map((x) => x.charAt(0).toUpperCase() + x.slice(1)); // generate methods to do what and how for (const [what, [op, preprocess]] of Object.entries(composers)) { // make plain version, e.g. pat._add(value) adds that plain value // to all the values in pat - Pattern.prototype['_' + what] = function (value) { + addToPrototype('_' + what, function (value) { return this.fmap((x) => op(x, value)); - }; + }); // make patternified monster version Object.defineProperty(Pattern.prototype, what, { @@ -1007,7 +1144,7 @@ function _composeOp(a, b, func) { // add methods to that function for each behaviour for (const how of hows) { - wrapper[how.toLowerCase()] = function (...other) { + const howfunc = function (...other) { var howpat = pat; other = sequence(other); if (preprocess) { @@ -1025,19 +1162,41 @@ function _composeOp(a, b, func) { } return result; }; + + for (const [controlname, controlfunc] of controlRegistry) { + howfunc[controlname] = (...args) => howfunc(controlfunc(...args)); + } + wrapper[how.toLowerCase()] = howfunc; } wrapper.squeezein = wrapper.squeeze; + for (const [controlname, controlfunc] of controlRegistry) { + wrapper[controlname] = (...args) => wrapper.in(controlfunc(...args)); + } + return wrapper; }, }); - // Default op to 'set', e.g. pat.squeeze(pat2) = pat.set.squeeze(pat2) - for (const how of hows) { - Pattern.prototype[how.toLowerCase()] = function (...args) { - return this.set[how.toLowerCase()](args); - }; - } + registerMethod(what, true, true); + } + + // Default op to 'set', e.g. pat.squeeze(pat2) = pat.set.squeeze(pat2) + for (const howLower of alignments) { + // Using a 'get'ted function so that all the controls are added + Object.defineProperty(Pattern.prototype, howLower, { + get: function () { + const pat = this; + const howfunc = function (...args) { + return pat.set[howLower](args); + }; + for (const [controlname, controlfunc] of controlRegistry) { + howfunc[controlname] = (...args) => howfunc(controlfunc(...args)); + } + return howfunc; + }, + }); + registerMethod(howLower, false, true); } // binary composers @@ -1049,36 +1208,36 @@ function _composeOp(a, b, func) { * .struct("x ~ x ~ ~ x ~ x ~ ~ ~ x ~ x ~ ~") * .slow(4) */ - Pattern.prototype.struct = function (...args) { + addToPrototype('struct', function (...args) { return this.keepif.out(...args); - }; - Pattern.prototype.structAll = function (...args) { + }); + addToPrototype('structAll', function (...args) { return this.keep.out(...args); - }; + }); /** * Returns silence when mask is 0 or "~" * * @example * note("c [eb,g] d [eb,g]").mask("<1 [0 1]>").slow(2) */ - Pattern.prototype.mask = function (...args) { + addToPrototype('mask', function (...args) { return this.keepif.in(...args); - }; - Pattern.prototype.maskAll = function (...args) { + }); + addToPrototype('maskAll', function (...args) { return this.keep.in(...args); - }; + }); /** * Resets the pattern to the start of the cycle for each onset of the reset pattern. * * @example * s(" sd, hh*4").reset("") */ - Pattern.prototype.reset = function (...args) { + addToPrototype('reset', function (...args) { return this.keepif.trig(...args); - }; - Pattern.prototype.resetAll = function (...args) { + }); + addToPrototype('resetAll', function (...args) { return this.keep.trig(...args); - }; + }); /** * Restarts the pattern for each onset of the restart pattern. * While reset will only reset the current cycle, restart will start from cycle 0. @@ -1086,12 +1245,12 @@ function _composeOp(a, b, func) { * @example * s(" sd, hh*4").restart("") */ - Pattern.prototype.restart = function (...args) { + addToPrototype('restart', function (...args) { return this.keepif.trigzero(...args); - }; - Pattern.prototype.restartAll = function (...args) { + }); + addToPrototype('restartAll', function (...args) { return this.keep.trigzero(...args); - }; + }); })(); // aliases @@ -1336,36 +1495,68 @@ export function pm(...args) { polymeter(...args); } -export const mask = curry((a, b) => reify(b).mask(a)); -export const struct = curry((a, b) => reify(b).struct(a)); -export const superimpose = curry((a, b) => reify(b).superimpose(...a)); +export const mask = curryPattern((a, b) => reify(b).mask(a)); +export const struct = curryPattern((a, b) => reify(b).struct(a)); +export const superimpose = curryPattern((a, b) => reify(b).superimpose(...a)); + +const methodToFunction = function (name, addAlignments = false) { + const func = curryPattern((a, b) => reify(b)[name](a)); + + withControls((controlname, controlfunc) => { + func[controlname] = function (...pats) { + return func(controlfunc(...pats)); + }; + }); + + if (addAlignments) { + for (const alignment of alignments) { + func[alignment] = curryPattern((a, b) => reify(b)[name][alignment](a)); + withControls((controlname, controlfunc) => { + func[alignment][controlname] = function (...pats) { + return func[alignment](controlfunc(...pats)); + }; + }); + } + } + + return func; +}; // operators -export const set = curry((a, b) => reify(b).set(a)); -export const keep = curry((a, b) => reify(b).keep(a)); -export const keepif = curry((a, b) => reify(b).keepif(a)); -export const add = curry((a, b) => reify(b).add(a)); -export const sub = curry((a, b) => reify(b).sub(a)); -export const mul = curry((a, b) => reify(b).mul(a)); -export const div = curry((a, b) => reify(b).div(a)); -export const mod = curry((a, b) => reify(b).mod(a)); -export const pow = curry((a, b) => reify(b).pow(a)); -export const band = curry((a, b) => reify(b).band(a)); -export const bor = curry((a, b) => reify(b).bor(a)); -export const bxor = curry((a, b) => reify(b).bxor(a)); -export const blshift = curry((a, b) => reify(b).blshift(a)); -export const brshift = curry((a, b) => reify(b).brshift(a)); -export const lt = curry((a, b) => reify(b).lt(a)); -export const gt = curry((a, b) => reify(b).gt(a)); -export const lte = curry((a, b) => reify(b).lte(a)); -export const gte = curry((a, b) => reify(b).gte(a)); -export const eq = curry((a, b) => reify(b).eq(a)); -export const eqt = curry((a, b) => reify(b).eqt(a)); -export const ne = curry((a, b) => reify(b).ne(a)); -export const net = curry((a, b) => reify(b).net(a)); -export const and = curry((a, b) => reify(b).and(a)); -export const or = curry((a, b) => reify(b).or(a)); -export const func = curry((a, b) => reify(b).func(a)); +export const set = methodToFunction('set', true); +export const keep = methodToFunction('keep', true); +export const keepif = methodToFunction('keepif', true); +export const add = methodToFunction('add', true); +export const sub = methodToFunction('sub', true); +export const mul = methodToFunction('mul', true); +export const div = methodToFunction('div', true); +export const mod = methodToFunction('mod', true); +export const pow = methodToFunction('pow', true); +export const band = methodToFunction('band', true); +export const bor = methodToFunction('bor', true); +export const bxor = methodToFunction('bxor', true); +export const blshift = methodToFunction('blshift', true); +export const brshift = methodToFunction('brshift', true); +export const lt = methodToFunction('lt', true); +export const gt = methodToFunction('gt', true); +export const lte = methodToFunction('lte', true); +export const gte = methodToFunction('gte', true); +export const eq = methodToFunction('eq', true); +export const eqt = methodToFunction('eqt', true); +export const ne = methodToFunction('ne', true); +export const net = methodToFunction('net', true); +export const and = methodToFunction('and', true); +export const or = methodToFunction('or', true); +export const func = methodToFunction('func', true); + +// alignments +// export const in = methodToFunction('in'); // reserved word :( +export const out = methodToFunction('out'); +export const mix = methodToFunction('mix'); +export const squeeze = methodToFunction('squeeze'); +export const squeezeout = methodToFunction('squeezeout'); +export const trig = methodToFunction('trig'); +export const trigzero = methodToFunction('trigzero'); /** * Registers a new pattern method. The method is added to the Pattern class + the standalone function is returned from register. @@ -1384,9 +1575,10 @@ export function register(name, func) { return result; } const arity = func.length; - var pfunc; // the patternified function - pfunc = function (...args) { + registerMethod(name); + + const pfunc = function (...args) { args = args.map(reify); const pat = args[args.length - 1]; if (arity === 1) { @@ -1402,8 +1594,12 @@ export function register(name, func) { .map((_, i) => args[i] ?? undefined); return func(...args, pat); }; - mapFn = curry(mapFn, null, arity - 1); - return right.reduce((acc, p) => acc.appLeft(p), left.fmap(mapFn)).innerJoin(); + mapFn = curryPattern(mapFn, arity - 1); + + const app = (acc, p) => acc.appLeft(p); + const start = left.fmap(mapFn); + + return right.reduce(app, start).innerJoin(); }; Pattern.prototype[name] = function (...args) { @@ -1428,7 +1624,7 @@ export function register(name, func) { // toplevel functions get curried as well as patternified // because pfunc uses spread args, we need to state the arity explicitly! - return curry(pfunc, null, arity); + return curryPattern(pfunc, arity); } ////////////////////////////////////////////////////////////////////// diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs index 08638d18..2142242d 100644 --- a/packages/core/test/pattern.test.mjs +++ b/packages/core/test/pattern.test.mjs @@ -45,6 +45,8 @@ import { rev, time, run, + hitch, + set, } from '../index.mjs'; import { steady } from '../signal.mjs'; @@ -204,7 +206,7 @@ describe('Pattern', () => { ), ); }); - it('can SqueezeOut() structure', () => { + it('can squeezeout() structure', () => { sameFirst( sequence(1, [2, 3]).add.squeezeout(10, 20, 30), sequence([11, [12, 13]], [21, [22, 23]], [31, [32, 33]]), @@ -252,7 +254,7 @@ describe('Pattern', () => { ), ); }); - it('can SqueezeOut() structure', () => { + it('can squeezeout() structure', () => { sameFirst(sequence(1, [2, 3]).keep.squeezeout(10, 20, 30), sequence([1, [2, 3]], [1, [2, 3]], [1, [2, 3]])); }); }); @@ -294,7 +296,7 @@ describe('Pattern', () => { ), ); }); - it('can SqueezeOut() structure', () => { + it('can squeezeout() structure', () => { sameFirst(sequence(1, [2, 3]).keepif.squeezeout(true, true, false), sequence([1, [2, 3]], [1, [2, 3]], silence)); }); }); @@ -929,6 +931,14 @@ describe('Pattern', () => { }); }); describe('alignments', () => { + it('Can combine controls', () => { + sameFirst(s('bd').set.in.n(3), s('bd').n(3)); + sameFirst(s('bd').set.squeeze.n(3, 4), sequence(s('bd').n(3), s('bd').n(4))); + }); + it('Can combine functions with alignmed controls', () => { + sameFirst(s('bd').apply(fast(2).set(n(3))), s('bd').fast(2).set.in.n(3)); + sameFirst(s('bd').apply(fast(2).set.in.n(3)), s('bd').fast(2).set.in.n(3)); + }); it('Can squeeze arguments', () => { expect(sequence(1, 2).add.squeeze(4, 5).firstCycle()).toStrictEqual(sequence(5, 6, 6, 7).firstCycle()); }); @@ -959,4 +969,16 @@ describe('Pattern', () => { sameFirst(s('a', 'b').hurry(2), s('a', 'b').fast(2).speed(2)); }); }); + describe('composable functions', () => { + it('Can compose functions', () => { + sameFirst(sequence(3, 4).fast(2).rev().fast(2), fast(2).rev().fast(2)(sequence(3, 4))); + }); + it('Can compose by method chaining operators with controls', () => { + sameFirst(s('bd').apply(set.n(3).fast(2)), s('bd').set.n(3).fast(2)); + }); + it('Can compose by method chaining operators and alignments with controls', () => { + sameFirst(s('bd').apply(set.in.n(3).fast(2)), s('bd').set.n(3).fast(2)); + // sameFirst(s('bd').apply(set.squeeze.n(3).fast(2)), s('bd').set.squeeze.n(3).fast(2)); + }); + }); }); diff --git a/packages/core/util.mjs b/packages/core/util.mjs index 3127e0d1..6ba8f397 100644 --- a/packages/core/util.mjs +++ b/packages/core/util.mjs @@ -139,7 +139,7 @@ export const removeUndefineds = (xs) => xs.filter((x) => x != undefined); export const flatten = (arr) => [].concat(...arr); export const id = (a) => a; -export const constant = (a, b) => a; +export const constant = curry((a, b) => a); export const listRange = (min, max) => Array.from({ length: max - min + 1 }, (_, i) => i + min);