Concern with merging exports #29

Closed
opened 2022-07-25 21:03:52 +00:00 by glen · 1 comment
Owner

The Pocomath architecture is based on bundling up signatures. Currently each type's version of each operation is in a separate file (although they could be clumped more, say by type and then topic group like arithmetic, relational, logical, cobinatorics, etc) and they are then collected via reexporting into clumps, and then collected into larger clumps, and then finally installed into a PocomathInstance.ed

The potential difficulty arises when clumps are aggregated that define different signatures for the same operation. Currently, these are exported as different values for the same identifer (the operation). But when you do

export * from './path/A.mjs'
export * from './path/B.mjs'

the resulting module can only have one value for an identifer 'foo'. So if A and B each put different signatures on foo, (at least) one of the contributions is going to be lost.

(Aside: JavaScript doesn't seem to complain about the conflict. But it does have some kind of resolution mechanism, which seems to sometimes end up with the module in which two different versions of foo are re-exported with export * having no definition of foo.)

So far, I have gotten everything to work by explicitly exporting the version of any conflicting signature that I want in such collecting-up modules. But in the last PR it became a bit of a pain, and it does not feel scalable.

Ideally in our setting, what we'd like is for the two versions of foo to merge as objects, since they presumably define disjoint signatures for the ultimate operation. But that is definitely not in the realm of JavaScript semantics.

So here are some alternatives:

  1. We chould change the notation, so that rather than exporting, for example, the identifier sqrt as an object with key Complex (for its signature 'Complex'), we would export the identifier sqrt_Complex with the specification of the implementation for that signature. Then if the signatures are disjoint, all of the symbols do just accumulate without conflict, and they can all be installed. This is basically what happened with types: identifier Types with key Complex became identifier Type_Complex, for basically the identical reasons. However, this approach does not seem very comfortable for the operations themselves: aside from its basic inelegance, the suffixes here would be full-blown signatures, rather than just a type identifier (which can much more easily be limited to a JavaScript identifier). Everything in a signature would have to be shoehorned into a valid identifier, which will end up just plain ugly. Plus we know the import/export doesn't actually complain about collisions, so we won't have an error check on unintentional signature clashes.

  2. We could add a utility to merge imports, something like

import FuseExports from '../utils/Fuse.mjs'
import * as A_stuff from './path/A.mjs'
import * as B_stuff from './path/B.mjs'
const exports = FuseExports(A_stuff, B_stuff)
export {...exports} // does this syntax work? I know there is a way to export
                    // a bunch of stuff at once

Then we can put all the clever merging and conflict checking we want into FuseExports. (In fact, we could switch back to putting types onto a single Types property, if that seems nicer.)

  1. We could switch to having all of our modules have a single default export which is a PocomathInstance, and then do all our merging with them:
import A from './path/A.mjs'
import B from './path/B.mjs'
export default PocomathInstance.merge(A,B)

An advantage here is that we can just use separate methods for types and operations, getting rid of special identifiers like Type_Complex altogether.
So a module that provides the type Complex might look like:

import PocomathInstance from '../core/PocomathInstance.mjs'

const instance = new PocomathInstance()
instance.addType('complex', {
  test: z => z && typeof z === 'object' && 're' in z && 'im' in z,
  before: ['Matrix'],
  from: {
    number: n => {re: n, im: 0},
    bigint: b => {re: b, im: 0n}
  }
})

export default instance

And a module that uses this might look like:

import Complex from './typePath/Complex.mjs' // note this is the PocomathInstance
                                             // with the type Complex defined
const instance = Complex.clone() // don't mess the base Complex instance for others
instance.add('foo', {
  Complex: ({complex}) => z => complex(fooReal(z.re), fooIm(z.im))
})
export default instance

This seems pretty clean, but does have a bit of boilerplate to it...

  1. Adopt a hybrid, in which individual pieces are exported as plain objects, like they are now, but in any case of potential merging, they are all installed into a PocomathInstance, and that's exported. So this is like 2, just the merging is done
    with PocomathInstance:
import PocomathInstance from '../core/PocomathInstance.mjs'
import A from './path/A.mjs'
import B from './path/B.mjs'
export default PocomathInstance.merge(A, B)

All that needs to be done here is to allow the merge static function of PocomathInstance to take any number of arguments, each of which can either be a plain object with operator/type identifiers, or another PocomathInstance, and have it merge them all together sanely. Then we won't rely on Javascript's default re-export merging, but there shouldn't be much code overhead.

Option 4 seems to be the way to go.

The Pocomath architecture is based on bundling up signatures. Currently each type's version of each operation is in a separate file (although they could be clumped more, say by type and then topic group like arithmetic, relational, logical, cobinatorics, etc) and they are then collected via reexporting into clumps, and then collected into larger clumps, and then finally installed into a PocomathInstance.ed The potential difficulty arises when clumps are aggregated that define different signatures for the same operation. Currently, these are exported as different values for the same identifer (the operation). But when you do ``` export * from './path/A.mjs' export * from './path/B.mjs' ``` the resulting module can only have one value for an identifer 'foo'. So if A and B each put different signatures on foo, (at least) one of the contributions is going to be lost. (Aside: JavaScript doesn't seem to complain about the conflict. But it does have some kind of resolution mechanism, which seems to sometimes end up with the module in which two different versions of foo are re-exported with export * having **no** definition of foo.) So far, I have gotten everything to work by explicitly exporting the version of any conflicting signature that I want in such collecting-up modules. But in the last PR it became a bit of a pain, and it does not feel scalable. Ideally in our setting, what we'd like is for the two versions of foo to _merge_ as objects, since they presumably define disjoint signatures for the ultimate operation. But that is definitely not in the realm of JavaScript semantics. So here are some alternatives: 1) We chould change the notation, so that rather than exporting, for example, the identifier `sqrt` as an object with key `Complex` (for its signature 'Complex'), we would export the identifier `sqrt_Complex` with the specification of the implementation for that signature. Then if the signatures are disjoint, all of the symbols do just accumulate without conflict, and they can all be installed. This is basically what happened with types: identifier Types with key Complex became identifier Type_Complex, for basically the identical reasons. However, this approach does not seem very comfortable for the operations themselves: aside from its basic inelegance, the suffixes here would be full-blown signatures, rather than just a type identifier (which can much more easily be limited to a JavaScript identifier). Everything in a signature would have to be shoehorned into a valid identifier, which will end up just plain ugly. Plus we know the import/export doesn't actually complain about collisions, so we won't have an error check on unintentional signature clashes. 2) We could add a utility to merge imports, something like ``` import FuseExports from '../utils/Fuse.mjs' import * as A_stuff from './path/A.mjs' import * as B_stuff from './path/B.mjs' const exports = FuseExports(A_stuff, B_stuff) export {...exports} // does this syntax work? I know there is a way to export // a bunch of stuff at once ``` Then we can put all the clever merging and conflict checking we want into FuseExports. (In fact, we could switch back to putting types onto a single Types property, if that seems nicer.) 3) We could switch to having all of our modules have a single default export which is a PocomathInstance, and then do all our merging with them: ``` import A from './path/A.mjs' import B from './path/B.mjs' export default PocomathInstance.merge(A,B) ``` An advantage here is that we can just use separate methods for types and operations, getting rid of special identifiers like `Type_Complex` altogether. So a module that provides the type Complex might look like: ``` import PocomathInstance from '../core/PocomathInstance.mjs' const instance = new PocomathInstance() instance.addType('complex', { test: z => z && typeof z === 'object' && 're' in z && 'im' in z, before: ['Matrix'], from: { number: n => {re: n, im: 0}, bigint: b => {re: b, im: 0n} } }) export default instance ``` And a module that uses this might look like: ``` import Complex from './typePath/Complex.mjs' // note this is the PocomathInstance // with the type Complex defined const instance = Complex.clone() // don't mess the base Complex instance for others instance.add('foo', { Complex: ({complex}) => z => complex(fooReal(z.re), fooIm(z.im)) }) export default instance ``` This seems pretty clean, but does have a bit of boilerplate to it... 4) Adopt a hybrid, in which individual pieces are exported as plain objects, like they are now, but in any case of potential merging, they are all installed into a PocomathInstance, and that's exported. So this is like 2, just the merging is done with PocomathInstance: ``` import PocomathInstance from '../core/PocomathInstance.mjs' import A from './path/A.mjs' import B from './path/B.mjs' export default PocomathInstance.merge(A, B) ``` All that needs to be done here is to allow the `merge` static function of PocomathInstance to take any number of arguments, each of which can either be a plain object with operator/type identifiers, or another PocomathInstance, and have it merge them all together sanely. Then we won't rely on Javascript's default re-export merging, but there shouldn't be much code overhead. Option 4 seems to be the way to go.
Author
Owner

Option 5:
Like option 4, but have the merging just be a little more clever so that you can always say

import PocomathInstance from '../core/PocomathInstance.mjs'
import * as A from './path/A.mjs'
import * as B from './path/B.mjs'
export default PocomathInstance.merge(A,B)

without having to worry about how A and B exported. The advantage here is that we can get rid of the magic names for type exports and instead export a type exactly as in option 3 above, but use it with less boilerplate like so:

export Complex from './typePath/Complex.mjs'
export const foo = {
  Complex: ({complex}) => z => complex(fooReal(z.re), fooIm(z.im))
})

and everything should just work.

I will give option 5 a try.

Option 5: Like option 4, but have the merging just be a little more clever so that you can always say ``` import PocomathInstance from '../core/PocomathInstance.mjs' import * as A from './path/A.mjs' import * as B from './path/B.mjs' export default PocomathInstance.merge(A,B) ``` without having to worry about how A and B exported. The advantage here is that we can get rid of the magic names for type exports and instead export a type exactly as in option 3 above, but use it with less boilerplate like so: ``` export Complex from './typePath/Complex.mjs' export const foo = { Complex: ({complex}) => z => complex(fooReal(z.re), fooIm(z.im)) }) ``` and everything should just work. I will give option 5 a try.
glen closed this issue 2022-07-29 04:42:05 +00:00
Sign in to join this conversation.
No Milestone
No project
No Assignees
1 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: glen/pocomath#29
No description provided.