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

(Optionally) automated environment via namespaces #137

Open
JAForbes opened this issue Apr 25, 2017 · 7 comments
Open

(Optionally) automated environment via namespaces #137

JAForbes opened this issue Apr 25, 2017 · 7 comments

Comments

@JAForbes
Copy link
Member

I find myself calling create and passing around different env's of types in many files, and I've only just started really leaning on sanctuary in a large project, and its going to get worse.

I've thought about different ways to structure this to prevent the verbosity, but I thought I'd at least vent the radical thought that keeps coming to mind.

What I really want, is an automated env. I don't want to pass env's around, I want to define a type and ideally I want sanctuary-def to manage the concatenation for me.

This came up when I was working on sum-type, I wanted to be able to nest union types, and I wasn't sure if that meant I needed to insert the previously created types into the env of any new types. So to date (I think), that's exactly what I do. Its technically still pure, it just automatically gives each type a new env that includes the previously defined types. I don't know if I actually need to do that, but whether or not I do, it at least makes me wonder, why don't we do this by default?

I don't want to have to think about order of operations across possibly 100's of modules.
I want to be able to write $.RecordType({ ... }) and know I can use that type anywhere now.

Now there's problems, maybe we don't want some subset of a project to include the environment from another part of a project. I think this is solved in programming languages via a namespace.

So what if we had $.namespace

// file1
// string -> Array ( Any -> Any ) -> ???
$.namespace('main', function([def, S, SumType]){
   $.NullaryType(...)
   $.UnaryType(...)
   $.RecordType(...)

   def(....)
   def(....)

   SumType(...)
})
//file2
// string -> Array ( Any -> Any ) -> ???
$.namespace('main', function([def]){
   $.NullaryType(...)
   $.UnaryType(...)
   $.RecordType(...)

   
})

And then in one place, we can initialise that namespace.

// String -> Array ({ checkTypes: Boolean, env }) -> ???
$.createNamespace('main', [
  $.create
  ,S.create
  ,SumType.create
])

Just walking through the signatures, $.createNamespace would accept a namespace name, and a list of functions that receive { checkTypes, env }, sanctuary-def would somehow initialize them for us, and manage concatenating the environment behind the scenes. This is very hand wavey! 😄

I'm not sure what $.createNamespace would return, I have a few ideas. One option, it could return a merged object of whatever each $.namespace returned, essentially giving us an analogue to modules. Another idea could be to return an interface that gives the caller control over exception handling (whether to throw, or write errors to a stream etc)

This API could let us do cool things, like use different namespaces for testing, or hot paths. Or control execution of code that depends on sanctuary-def types. Now we can toggle env for the entire namespace (across files) in one place. We also don't need to concern ourselves with concatenating different type arrays, because namespaces would automatically do that for us.

It's very opinionated, and maybe would be better off as a library, but I don't know if it would be possible to do this in user-land.

Its a very broad debatable idea, but seems interesting to me. I'd love to hear some criticism!

@gabejohnson
Copy link
Member

gabejohnson commented Apr 25, 2017

How about

// file1.js
// string -> StrMap Type -> ???
export default $.environment(({def, $, SumType}) => ({
   TypeA: $.NullaryType(...),
   TypeB: $.UnaryType(...),
   TypeC: $.RecordType(...),
   fn1: def(....),
   fn2: def(....),
   TypeD: SumType(...)
}));

// file2.js
// string -> Array ( Any -> Any ) -> ???
export default $.environment(({$}) => ({
   TypeA: $.NullaryType(...),
   TypeB: $.UnaryType(...),
   TypeC: $.RecordType(...),
}));

// main.js
import env1 from 'file1';
import env2 from 'file2';

const makeNs = $.namespace({
  def: S.create,
  $: $.create,
  SumType: SumType.create
});

const ns = makeNs('main', S.concat(env1, env2)); // env2 overwrites bindings from env1
const { bindings: {TypeA} } = ns;
const { TypeA: f1TypeA } = makeNs('', env1);

$.environment or $.namespace could also concatenate the bindings with whichever standard/global types/functions exist.

@JAForbes
Copy link
Member Author

JAForbes commented Apr 26, 2017

I like that revision!

Maybe $.namespace could accept args for documentation base urls, and package name prefixes, and the namespace itself could autoprefix type and function names with package prefixes. Potentially we could just provided the hash, or final path of a documentation url per type.

makeNs(
  'my-package/main'
  ,'https://my-package.github.io/docs/'
  ,{ checkTypes: true, env }
)

That would automatically prefix any documentation urls in that namespace with that base url, and any type with that package prefix.

@gabejohnson
Copy link
Member

Maybe $.namespace could be:

namespace :: Boolean -> StrMap Function -> String -> String -> Environment

The first argument is checkTypes. I'm thinking it's going to be the most stable argument. Usually you're going to want this setting to be these same everywhere.

@davidchambers
Copy link
Member

I don't like the idea of $.namespace being stateful, so I like Gabe's revision. :)

I don't want to have to think about order of operations across possibly 100's of modules. I want to be able to write $.RecordType({ ... }) and know I can use that type anywhere now.

This is the heart of the matter, I think.

I believe there is an approach which avoids the ordering problem:

  1. Define all the types one will use in one's application.
  2. Create an environment containing all these types.
  3. Export a customized def function and S module for use throughout one's application.
  4. Use the internally defined def and S throughout one's application.

Here's a concrete example:

//  app/index.js

const {S, def} = require('./env');

...
//  app/env.js

const S = require('sanctuary');
const $ = require('sanctuary-def');

const CountryCode = require('./types/CountryCode');
const User = require('./types/User');
const UserId = require('./types/UserId');

//    checkTypes :: Boolean
const checkTypes = true;

//    env :: Array Type
const env = S.env.concat([CountryCode, User, UserId]);

exports.def = $.create({checkTypes, env});
exports.S = S.create({checkTypes, env});
//  app/types/CountryCode.js

const $ = require('sanctuary-def');

module.exports = $.NullaryType(...);
//  app/types/User.js

const $ = require('sanctuary-def');

const UserId = require('./UserId');

module.exports = $.RecordType({
  id: UserId,
  firstName: $.String,
  lastName: $.String,
});
//  app/types/UserId.js

const $ = require('sanctuary-def');

module.exports = $.NullaryType(...);

There are certainly ways in which we could make this more convenient, but the most important thing is to first establish the correct module layering:

    +-----------------------------------+
    |            Application            |
    +-----------------------------------+
    |            Environment            |
    +--------+--------+--------+--------+
    |  Type  |  Type  |  Type  |  Type  |
    +--------+--------+--------+--------+

- - - - - - - - - - - - - - - - - - - - - - -

    +-----------------------------------+
    |           sanctuary-def           |
    +-----------------------------------+

If you agree with this layering, I'd love to know how well it works in your application, @JAForbes.

@JAForbes
Copy link
Member Author

JAForbes commented May 8, 2017

@davidchambers Yeah I like this style, I saw you demonstrated it in the gitter after this discussion had taken place over there and I thought it was brilliant!

I'm not sure if it works in the context of sum-type, we often want to define model types and page specific types in close proximity to the view because their helpful for modelling. And we will probably want to create functions using def that are aware of these types, defined in the same file. So we end up needing to create new def functions in each route.

I think it would be a shame to need to move these page specific types to a separate file.

The clean division your presenting is more difficult when we have hyper specific types for a particular page (model, enumerations of potential states and values specific to that page), not generic reusable types (like user, email, uuid etc).

If you read Elm code you'll see this pattern often, hyper specific types to a particular component.

It seems like our app specific sanctuary and sanctuary-def modules would be defined too early in this case.

I don't have any solutions, just voicing a concern 😄

@davidchambers
Copy link
Member

If you read Elm code you'll see this pattern often, hyper specific types to a particular component.

This helps me to understand your requirements. :)

I suggest that you experiment in your application and comment again in this thread once you arrive at a satisfactory solution.

@JAForbes
Copy link
Member Author

JAForbes commented May 9, 2017 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants