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

What is the logic behind when we use Tile and when we use Tile.prototype? #86

Closed
etodanik opened this issue Aug 31, 2022 · 9 comments
Closed

Comments

@etodanik
Copy link

export class Tile extends defineHex({
  orientation: Orientation.POINTY, // pointy orientation
  offset: -1, // odd rows are offset
}) {
  // ...
}

When we do new Grid(Tile... we just use Tile, but in some places like distance() we use Tile.prototype which seems to be the legacy approach. Is this by design and I'm missing some logic / motivation? Or is this "technical debt"?

@flauwekeul
Copy link
Owner

It is by design, but I understand the confusion. I could change distance() (and other functions that require the prototype) to just accept a Hex class/constructor. Then internally, the function can get the prototype property. However, the function doesn't need the class/constructor, it needs the prototype. But from a user perspective having to pass Tile.prototype may be weird... What do you suggest?

@etodanik
Copy link
Author

So now that I'm paying it some second thought. I like that it just needs two quite static things, orientation and offset. I think that using Tile everywhere would be a downgrade in testability, developer experience e.t.c

However, passing prototype anywhere is quite weird, as well. I think I know where the source of weirdness is, give me a few minutes I'll prepare a minimalistic example.

@etodanik
Copy link
Author

etodanik commented Aug 31, 2022

So here are my thoughts in a Playground Link

If you run it, you'll see what I mean.

The tldr; of it is that we keep a simple { orientation, offset} type on the input of stuff like distance() (basically a Partial<> of HexSettings?) but instead of passing around prototypes, we create a static getter getHexSettings(), and instead of clever scope stuff for prototypes, defineHex now simply implements some static readonly stuff for our abstract class Hex. Those static thingies can become abstract as well when the feature lands in the TS language, but it all works already.

Edit:
Ah, I forget that this is pure JS =) Haha
Would that be negotiable?

Let me see if I can make a little sandbox of something similar on pure JS. The tldr; is using prototypes directly is super strange & I never had to do it much anywhere as of late.

And it becomes even more strange when consuming this lib in any TypeScript environment.

Edit 2:
Here's the same in pure JS with a bit less fancy stuff like making Hex into abstract Hex, but just as easily avoiding passing around prototypes.

And you can still define the output of getHexSettings() as returning HexSettings and then require Partial<HexSettings> or just offset/orientation in various functions.

class Hex {
  // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/static
  // supported in all evergreen browsers
  static orientation; 
  static offset;

  static getHexSettings() {
    return {
      orientation: this.orientation,
      offset: this.offset,
    };
  }
 
  // .. lots of other cool stuff
}


function defineHex(hexOptions) {
  const { orientation, offset } = {
    /* ...defaultHexSettings, */ ...hexOptions,
  };

  return class extends Hex {
    static orientation = orientation;
    static offset = offset;
  };
}

const Tile = defineHex({
  orientation: "POINTY",
  offset: -1,
});

console.log(Tile.orientation); // "POINTY"
console.log(Tile.offset); // -1
console.log(Tile.getHexSettings()); // { offset: -1, orientation: "POINTY" }

@flauwekeul
Copy link
Owner

Just a quick reply because I don't have time: how would you get orientation for example from the instance? Tile.constructor.orientation is possible, but I think that's even more weird than using Tile.prototype.

I don't think hex settings are static. They're shared by all instances, that's why they're in the prototype.

@etodanik
Copy link
Author

etodanik commented Aug 31, 2022

Yes, you're right: Tile.constructor.orientation is also awkward.

But essentially what you have right now is getters that get their scope not even from the class itself but from the scope of the function that created the class.

I think there are two separate problems here:

  • Semantics: You don't really want Hex or Tile in distance(). distance() is a mathematical pure function and all it wants is some HexSettings the fact that the prototype satisfies that is purely coincidental IMHO. It somehow bothers me - and it's a bit clunkier to work around when consuming the lib. HexSettings are easier to serialize, pass around, store, send over the wire, e.t.c - You can't do any of that with a prototype. Of course, we need an easy way to also pass something from Tile to make distance know what the settings are, but IMHO something like Tile.getSettings() would still be a good idea. Right now if I simply decide to pass a perfectly fine object with { offset, orientation } to distance it will swear at me in Typescriplish: Argument of type '{ offset: -1; orientation: Orientation; }' is not assignable to parameter of type 'Pick '. Object literal may only specify known properties, and 'orientation' does not exist in type 'Pick '.
  • Implementation, how do we implement the right semantics? That's a very mechanical question, it can probably have many possible answers.

Now - if we can agree on the semantics, I think we could come up with solutions. But do we agree on the semantics?

To me as an end user this lib keeps feeling weird, and the source of this weirdness many times is because of passing either Tile around, or its' prototype to functions that seemingly don't REALLY need it. I'd much rather pass around a serializable HexSettings object than a prototype around.

Also for testing, I have loads of use cases where I'd love to do:
toCube(settings, hex) where settings are a simple HexSettings object. It feels unnecessary to be yelled at by TypeScript that I should have given it a prototype (when literally no unique property of a prototype is needed. Just the settings)

@flauwekeul
Copy link
Owner

We agree on the semantics 🙃 I'll need some time to think about this. I've experimented a lot in earlier versions with this and the current solution with the prototype was, then, the best of the worst. But I agree it's still a bit meh.

@flauwekeul
Copy link
Owner

flauwekeul commented Sep 3, 2022

A relatively simple solution would be to change the functions that require (part of) the prototype to just require hex settings. It seems these functions are: pointToCube(), offsetToCube(), hexToOffset() (this actually needs a hex instance), toCube() and distance() (or did I miss any?).

However, that would only change those functions' signatures, but you'd still have to pass either a hex instance or Hex.prototype. I could add a static getter settings that can be used like so:

class Tile extends Hex {
  get dimensions(): Ellipse {
    return createHexDimensions(30)
  }

  get orientation(): Orientation {
    return Orientation.FLAT
  }
}

Tile.settings
// {
//   dimensions: { xRadius: 30, yRadius: 30 },
//   offset: -1,
//   orientation: "FLAT",
//   origin: {x: 0, y: 0}
// }

distance(Tile.settings, [0, 0], [10, 10])

Would this remove some (or all) weirdness in using Honeycomb for you?

@etodanik
Copy link
Author

etodanik commented Sep 3, 2022 via email

flauwekeul added a commit that referenced this issue Sep 3, 2022
Also change type signatures of `pointToCube()`, `offsetToCube()`, `toCube()` and `distance()` so that they require (part of) hex settings instead of the prototype.
Closes #86.
github-actions bot pushed a commit that referenced this issue Sep 3, 2022
# [v4.0.0-beta.8](v4.0.0-beta.7...v4.0.0-beta.8) (2022-09-03)

## ✨ New Features
- [`97e2838`](97e2838)  add static &#x60;settings&#x60; prop to &#x60;Hex&#x60; (Issues: [`#86`](#86))

# [4.0.0-beta.8](v4.0.0-beta.7...v4.0.0-beta.8) (2022-09-03)
@github-actions
Copy link

github-actions bot commented Sep 3, 2022

🎉 This issue has been resolved in version 4.0.0-beta.8 🎉

The release is available on:

Your semantic-release bot 📦🚀

@etodanik etodanik closed this as completed Sep 4, 2022
github-actions bot pushed a commit that referenced this issue Oct 4, 2022
# [v4.0.0](v3.1.8...v4.0.0) (2022-10-04)

## ✨ New Features
- [`e08bc46`](e08bc46)  add, update and remove several Grid methods
- [`fbac697`](fbac697)  add some methods to Grid
- [`f74f0d9`](f74f0d9)  add completeCubeCoordinates() function
- [`0db7e22`](0db7e22)  add translate() function
- [`442e723`](442e723)  add pixelWidth and pixelHeight getters to Grid
- [`6b29a12`](6b29a12)  Hex is now a class
- [`5825ff0`](5825ff0)  update grid-related code to use Hex class
- [`6fc63cb`](6fc63cb)  rename functions and add util
- [`e3cafe0`](e3cafe0)  Hex&#x27;s toString() returns constructor name
- [`b369f1b`](b369f1b)  hexes now only have q and r in the instance
- [`46ab7f2`](46ab7f2)  grids now also accept iterables of coordinates
- [`6acb7e7`](6acb7e7)  make &#x60;hexPrototype&#x60; public in &#x60;Grid&#x60;
- [`97e2838`](97e2838)  add static &#x60;settings&#x60; prop to &#x60;Hex&#x60; (Issues: [`#86`](#86))

## 🐛 Bug Fixes
- [`38daeb0`](38daeb0)  fix Grid&#x27;s traverse() (Issues: [`#84`](#84))
- [`5b6f730`](5b6f730)  fix release
- [`b5a3895`](b5a3895)  another attempt to fix the release
- [`8f09aa4`](8f09aa4)  actually build the project in ci 🥲
- [`921726d`](921726d)  don&#x27;t add /dist to git 🫤
- [`8115d61`](8115d61)  use prepare hook for husky
- [`8e5d2b3`](8e5d2b3)  a spiral&#x27;s radius now excludes the center
- [`3ed0fa5`](3ed0fa5)  fix bug in pointToHex()

# [4.0.0](v3.1.8...v4.0.0) (2022-10-04)

### Bug Fixes

* **grid:** fix bug where internal hexes were cleared after Grid run() was called ([cb6c65d](cb6c65d)), closes [#67](#67)
* **grid:** fix incorrect width or height calculation in Grid.rectangleFromOpposingCorners() ([3b4bb7c](3b4bb7c))
* **grid:** fix neighborOf() (and functions that use it like move()) ([3b8cdf6](3b8cdf6))
* **grid:** grid's update() now always returns a grid that iterates over hexes in its store ([b2a0298](b2a0298)), closes [#68](#68)
* **grid:** grids are now iterable again (e.g.: `[...grid]` or `for (const hex of grid) {})`) ([c142a68](c142a68))
* **hex:** createHex() now also accepts tuple coordinates ([8f5196e](8f5196e))
* **hex:** fix typing issue for createHexPrototype() ([d6e24b4](d6e24b4))
* **hex:** when overriding a hex prototype method, `this` is now correctly typed ([8df5488](8df5488)), closes [1#comment116992054_66162731](https://github.com/1/issues/comment116992054_66162731)
* move (internal) util to fix circular dependency ([be57fee](be57fee))

### Code Refactoring

* **grid:** rename at() to add() and make it accept multiple coordinates ([e63f650](e63f650))

### Features

* add benny for running benchmarks (in node) ([63a932f](63a932f))
* add parcel-bundler and playground ([2845088](2845088))
* **coordinates:** expand the HexCoordinates type to include a tuple of axial or cube coordinates ([ca41673](ca41673))
* createHexPrototype() adds several readonly properties (WIP) ([6af96e1](6af96e1))
* first functionality of v4.0 (WIP) ([fcd005a](fcd005a))
* fix reference to type declaration and improve typing of `createHexPrototype()` ([2c53678](2c53678))
* **grid/neighborof:** returns a hex instead of just coordinates ([fa9f2f0](fa9f2f0))
* **grid/transduce:** add transduce function ([efd9c2e](efd9c2e))
* **grid/transducers:** add inGrid() transducer ([97660e4](97660e4))
* **grid/transducers:** add tap() ([28797cf](28797cf))
* **grid/traversers:** add repeatWith traverser ([8e400af](8e400af))
* **grid/traversers:** re-implement rays, ring and spiral ([9c84121](9c84121))
* **grid/traversers:** revert back to outputting arrays ([282f1f1](282f1f1))
* **grid:** a grid can now be created with an optional traverser ([e56ced8](e56ced8))
* **grid:** add distance function and method that returns the amount of hexes between 2 hexes ([7a2de46](7a2de46))
* **grid:** add from() static method that accepts an iterable of hexes and returns a new grid ([83b25ad](83b25ad))
* **grid:** add GridStore type and make sure the toString() Hex method is called to create an id ([99192b2](99192b2))
* **grid:** add HexCache class and use it as a start to cache hexes in grid ([2b4afeb](2b4afeb))
* **grid:** add hexes() and cursor() methods to Grid ([5738045](5738045))
* **grid:** add map() method to Grid ([3716e6c](3716e6c))
* **grid:** add NoOpCache and use it as the default cache in Grid ([30d7a87](30d7a87))
* **grid:** add optional argument to move() traverser to move in the same direction multiple times ([f8b96ad](f8b96ad))
* **grid:** add rays() traverser ([50e707d](50e707d))
* **grid:** add ring() traverser ([256879f](256879f))
* **grid:** add setStore helper function and add set method to GridStore interface ([10125c1](10125c1))
* **grid:** add size getter and hasHex() method ([eb41e71](eb41e71))
* **grid:** add spiral() traverser ([d433af0](d433af0))
* **grid:** add update() method to Grid ([ec0dfce](ec0dfce))
* **grid:** call copy() on the initial cursor hex in Grid.traverse() ([98c7054](98c7054))
* **grid:** change how a grid's store is set ([2376845](2376845))
* **grid:** grids can now be created/traversed with a single traverser or an array of traversers ([890ce96](890ce96))
* **grid:** improve types and name some anonymous functions for better debugging ([383dfbd](383dfbd))
* **grid:** improve typing of Grid methods and traverse commands ([193531d](193531d))
* **grid:** line() now accepts either "line as vector" options or "line between" options ([2fe7f1d](2fe7f1d))
* **grid:** make CompassDirection a union type ([6a65b87](6a65b87))
* **grid:** more or less settled on Grid API ([2cd5c1e](2cd5c1e))
* **grid:** move traversers to traversers folder and add utils folder with forEach and map ([751be5a](751be5a))
* **grid:** pass cursor along with hexes in getPrevHexState() for a ~20% performance increase ([a90f083](a90f083))
* **grid:** pass getHex() function to all traversers, this way they can use the cache ([64ec33b](64ec33b))
* **grid:** prevent iterators to be run again when run() is called more than once ([2e39a3b](2e39a3b))
* **grid:** rectangle() traverser and grid method now accept opposing corners as arguments ([b8bab92](b8bab92))
* **grid:** reimplement traverse to not be a generator ([3a7a09e](3a7a09e))
* **grid:** remove map method, add filter and takeWhile methods and add inStore helper function ([956a2a0](956a2a0))
* **grid:** remove move() alias of line() and update signature of line() ([5205e93](5205e93))
* **grid:** rename HexCache to CoordinatesCache and expand API, add toString() to hex ([6a0ca15](6a0ca15))
* **grid:** rename the move() traverser to line() and make move() an alias ([7663738](7663738))
* **grid:** restrict Grid.traverse() to the hexes in the grid (from the previous iteration, if any) ([c25249b](c25249b))
* **grid:** rewrite the rectangle traverser and update Grid.rectangle() to use it, add Compass class ([3e0ca95](3e0ca95))
* **grid:** subsequent traverse() calls now stop when attempting to traverse outside the grid ([1aa8f73](1aa8f73))
* **grid:** traverse functions don't create hexes anymore ([bdfb22b](bdfb22b))
* **grid:** traverse() now also accept transducers ([8efa19d](8efa19d))
* **grid:** traversers no longer accept both a `start` and `at` option, only either or none (XOR) ([231acf6](231acf6))
* **grid:** update grid rectangle method to only within previous grid ([b242c94](b242c94))
* **grid:** update rectangle() to behave as most traversers should ([5415029](5415029))
* **grid:** update ring() to behave as most traversers should ([e1cdc81](e1cdc81))
* **grid:** use single Compass enum, make move() accept ambiguous directions, improve grid.rectangle ([2bf8d1e](2bf8d1e))
* **grid:** various changes, mainly traverse() and update() methods ([a79be39](a79be39))
* **grid:** WIP: rewrite grid things to use iterables and transducers ([3c97979](3c97979))
* **hex,grid:** add round() and pointToCube() functions and pointToHex() method to Grid ([b302c08](b302c08))
* **hex,grid:** traversers return hexes (as before), fix move() traverser, add neighborOf() function ([b2da583](b2da583))
* **hex:** add center() function and method ([c0730b5](c0730b5))
* **hex:** add corners(), width() and height() functions ([e9e98da](e9e98da))
* **hex:** add equals() method to hex prototype ([f519448](f519448))
* **hex:** add functions to convert hexes to points ([7e1e3da](7e1e3da))
* **hex:** add isHex() function and overload corners() to either accept a hex or hex settings ([893c829](893c829))
* **hex:** createHex() now also accepts a hex (instance) and if it does copies it ([43d0109](43d0109))
* **hex:** hexCoordinates now also include offset coordinates ([dd39813](dd39813))
* **hex:** normalize and assert all required hex prototype properties in createHexPrototype() ([9cf0511](9cf0511))
* **hex:** rename createToPoint() to hexToPoint() and make it accept a hex ([4227737](4227737))
* **hex:** rename offsetToAxial() to offsetToCube() and make it return cube coordinates ([52d89f8](52d89f8))
* **hex:** rename size to dimensions and normalize dimensions in createHexPrototype() ([4c08684](4c08684))
* **hex:** set hex origin relative to center of hex ([0db666d](0db666d))
* **hex:** store s cube coordinate when passed to createHex, otherwise use getter ([9a9b4a9](9a9b4a9))
* **hex:** the equals() function and hex method can now compare different kinds of hex coordinates ([5dc14d0](5dc14d0))
* **hex:** toString() now also accepts cube coordinates ([1c552c4](1c552c4))
* **hex:** use axial coordinates by default ([775e711](775e711))
* remove cartesian coordinates for now ([dc959e7](dc959e7))
* remove transducers ([072ead5](072ead5))
* set [Symbol.toStringTag] on Grid and hex prototype ([a189f77](a189f77))
* **traversers/line:** add support for "stop" coordinates and update tests ([2cf143b](2cf143b))
* **traversers/ring:** it now also accepts a radius ([fafdcee](fafdcee))

### Performance Improvements

* **grid:** improve grid creation ([0748558](0748558))
* **grid:** replace generators with iterable-returning functions ([20655d1](20655d1))
* tiny improvements ([a58c7eb](a58c7eb))

### BREAKING CHANGES

* **grid:** When rectangle() isn't passed `start` coordinates it now excludes its first hex
(the cursor, usually `{ q: 0, r: 0 }`). In other words: you probably want to pass `start`
coordinates when using rectangle()
* **grid:** Remove move() alias of line(). line() now accepts an object with options: `direction`, `length`, `at` and `start`.
* **grid:** The at() traverser is renamed to add()
* **hex:** offsetToAxial() is now offsetToCube()
* **hex:** toString() now only accepts axial coordinates
* **grid:** Remove the of() static grid method, because it's a rather redundant as it does the
same as the constructor. Also update the inStore function to be used directly as a grid iterator
method callback (before change: `grid.each(inStore())`, after change: `grid.each(inStore)`). Remove
setStore() because it's ambiguous how the store should be set: hexes could be removed/added/updated
from the store.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants