node-elm-repl
v4.1.0
Published
a wrapper for elm-repl intended to be used from node.js
Downloads
25
Readme
node-elm-repl
- Intro
- Installation
- Description
- Running tests
- How to use it with Node.js? (JS API)
- How to deconstruct Type Definitions in JS? (JS API) (a separate page)
- How to use it with CLI? (Command-Line API)
- How to contribute?
- How it was done? (a separate page)
Intro
Actually the name may confuse you, so I would make it clear from the start, that technically what you see here is TOTALLY NOT the REPL. At least, by itself. It is the replacement for the REPL for those who need to know the evaluated values together with their types in their entirety (not just string-encoded boring types, but a structures defining the type, like... JSON Elm Type AST) for Elm expressions in JavaScript environment. Also, this tool may help you make some REPL... in JavaScript.
So, this tool would help you if, and only if, all of these points satisfy:
- You are interested in Elm language;
- You are writing something in JavaScript for the Elm Language, for example a plugin for some Electron-driven IDE / Code Editor;
- In JavaScript, you need to know the values and/or the returned structured types definitions of several Elm expressions with a single
elm-make
compilation cycle (for the moment, one cycle takes about 200-300ms in average, except rare first-run cases, taking up to 3 seconds); - Also, you are willing to do everything mentioned above offline, both for you and the user (except the case when
elm-make
auto-downloads required packages);
If these points are applicable to you, but you are still uncertain if you need this, please read The "Modern Binary Reverse Engineering with Node.js for Elm" Article (written by me) which describes in the very details, what is done here and how it works. Another way for you, in this case, is just to use this binary tool which was developed later than this one, and which is driven by Haskell (which is an advantage), so has a compiled binary (which is an advantage) and uses the "core" code to get the type information (which is an advantage), but has no ability to get values (which is whatever) and only has types in their stringified form (which is a disadvantage, but may be implementing it for an author is just a matter of time).
In short, when you use this:
new Repl({ elmVer: '0.18.0' })
.getTypes([], [
'1 + 1',
'\\a b -> a + b'
])
.then(types => console.dir(types, { depth: null }));
In response you get this:
[ { type: "var", name: "number" },
{ type: "lambda",
left: { type: "var", name: "number" },
right:
{ type: "lambda",
left: { type: "var", name: "number" },
right: { type: "var", name: "number" } } } ]
Or when you use this:
new Repl({ elmVer: '0.18.0' })
.getTypesAndValues([], [
'1 + 1',
'\\a b -> a + b'
])
.then(typesAndValues => console.dir(typesAndValues, { depth: null }))
.catch(err => console.error(err));
Then you get this:
{ types: [ ... <same-as-above> ... ],
values: [
"2",
"<function>"
] }
It is also possible to specify a complete module to parse, so:
new Repl({ elmVer: '0.18.0',
workDir: 'my/source/dir' })
.parseModule('MyModule.MyInnerModule')
.then(parsedModule => console.dir(parsedModule, { depth: null }))
.catch(err => console.error(err));
Will give you:
{
"version": {
"major": 0,
"minor": 18,
"patch": 0
},
"package": {
"name": "project",
"user": "user"
},
"imports": [ ... <a-list-of-imports> ... ],
"exports": [ ... <a-list-of-exports> ... ],
"types": [ ... <a-list-of-types> ... ]
...
}
Use the Types documentation to help you investigate the inner structure of such constructs.
And, the JS grammar for the .elmi
files is just lying here, in case you need it separately. It is accessible from JS with the Repl.Parser.parse(elmiFileBuffer)
. And the Kaitai Struct Format is defined for .elmi
files as well. And the XML version for Synalize it! just for the sake of joy.
Installation
npm install node-elm-repl
If this command fails, it's a known npm
issue, please try:
npm install node-elm-repl --no-bin-links
You need to have the fully working Elm project in current directory (by default), or in the place where you plan to use this library, or in the directory you specify with workDir
(a.k.a. --work-dir
in CLI) option, so before using it, please ensure to run there:
elm-package install
Then, you may test it with npm test
or with creating the test-file:
touch ./test-repl.js
And filling it with the minimal example (it works only with defaults, i.e. when Elm configuration wasn't changed after elm-package install
execution and elm-package.json
also wasn't manually modified, so user
/project
is the name of your current project at this moment):
var Repl = require('node-elm-repl');
new Repl({ // options
elmVer: '0.18.0', // your exact elm-compiler version
}).getTypes(
[ /* imports */ ],
[ /* expressions */
'\\a b -> a + b'
]
).then(function(types) { // getTypes returns the Promise which resolves to array
console.log(Repl.stringifyAll(types).join('\n'));
});
If everything's ok, you should get:
> node ./test-repl.js
number -> number -> number
Description
This package is a very specific and weird package.
So if you (yet) don't care about Elm language, you shouldn't be interested. Actually, even if you are a big fan of the Elm language (like me, since 0.15), there's totally no guarantee that this package could be interesting to you too.
Why? Because it actually does exactly what elm-repl
does. Even less. It just gets the types of expressions, no values (HAHA NO, with last updates it also extracts values, so now it has just the same functionality as REPL has, ensure to look through the documentation below!). But... what... changes... everything... is... this package does it completely with node.js!!
And it calculates all the expressions types you asked for just with one single call of elm-make
(original elm-repl
currently recompiles everything on every new input).
This makes this REPL three times faster and three times awesomer, since it gives you access to the JSONified structure of the type, instead of just a boring string.
How it got to be so fast and good? Read How it was done? below for the nice story and some technical details.
NB: If you find a case when some complex construct is not supported in this project, please don't panic, just follow at least some of the steps described in Contribute section.
Running tests
It's better to ensure that binary parser works with your elm version etc., so please first run this in the directory where you've installed the package:
npm install
npm install -g mocha
npm test
NB: When
elm-stuff
was cleaned up, the very first call toelm-make
in this directory takes longer than 2 seconds since it checks package availability and installs them sometimes, so the very first test always takes much longer than other 33 tests, which in their case are satisfyingly fast; so please take into consideration thatelm-stuff
is cleaned for every test run and timeout is set to 10 seconds just to make tests fair for clean environment, but there's a high chance that in your environment you won't clean upelm-stuff
so frequently, so absolutely any call should take less than 500ms.
You may see it for yourself, if you run instead
test-dirty
:
npm run test-dirty
Now any call should take no more than 500ms (usually less than 300ms).
How to use it in Node.js environment?
You may easily install the project using:
npm install node-elm-repl
Repl.getTypes
Then, in your JS file, and in the same directory where you have elm-package.json
or where you store your .elm
modules, if you have any, just do something like:
var Repl = require('node-elm-repl');
new Repl({ // options, defaults are listed:
workDir: '.', // working directory
elmVer: '0.18.0', // your exact elm-compiler version
user: 'user', // specify github username you used in elm-package.json
package: 'project', // specify project name you used in elm-package.json
packageVer: '1.0.0' // specify project version you used in elm-package.json
}).getTypes(
[ // imports:
'List as L',
'Maybe exposing ( Maybe(..) )'
],
[ // expressions:
'L.map',
'L.foldl',
'Just',
'Nothing',
'1 + 1',
'\\a b -> a + b'
]
).then(function(types) { // getTypes returns the Promise which resolves to array
// see ./Types.md for the details on every type definition
console.log(Repl.stringifyAll(types).join('\n'));
}).catch(console.error);
And when you run this script in the console you should see something like:
(a -> b) -> List a -> List b
(a -> b -> b) -> b -> List a -> b
a -> Maybe.Maybe a
Maybe.Maybe a
number
number -> number -> number
N.B.: Here you explicitly convert the received types descriptors to their string
-type text-friendly versions—but before this conversion took place, they actually were very detailed and descriptive JavaScript objects. So, if you need more than just a boring string representation of a type, you may follow to the Types API documentation, to know what exactly to expect from object representation of the type definition.
REPL Options
Repl
constructor accepts several options:
workDir
— specify working directory for execution, for example where the.elm
files you use are located, or where you have yourelm-package.json
;elmVer
— the exact elm-version you use (default:'0.18.0'
);user
— your github username specified inelm-package.json
(default:'user'
);package
— your project specified inelm-package.json
(default:'project'
);packageVer
— the version of your project fromelm-package.json
(default:'1.0.0'
);keepTempFile
— for debugging purposes, if truthy, then do not delete.elm
files after compilation;keepElmiFile
— for debugging purposes, if truthy, then do not delete.elmi
files after compilation;
It's not the only method of Repl
class:
Repl.getValues
Repl.getValues
returns the array of values in the same manner, so:
var Repl = require('node-elm-repl');
new Repl({
// ... your options
}).getValues(
[ // imports:
'List as L',
'Maybe exposing ( Maybe(..) )'
],
[ // expressions:
'L.map',
'Nothing',
'1 + 1',
'1.01',
'"f"++"a"',
'100 // 2'
'L.range 1 4',
'\\a b -> a + b'
]
).then(function(values) { // getValues returns the Promise which resolves to array
console.log(values).join('\n');
}).catch(console.error);
will show in console:
<function>
Nothing
2
1.01
"fa"
[1,2,3,4]
50
<function>
Repl.getTypesAndValues
Repl.getTypesAndValues
returns both types and values for the cases when you need both. In this moment the Node-REPL becomes as powerful as it's father and, can't resist to mention it again, at least three times faster.
var Repl = require('node-elm-repl');
new Repl({
// ... your options
}).getTypesAndValues(
[ // imports:
'List as L',
'Maybe exposing ( Maybe(..) )'
],
[ // expressions:
'L.map',
'Nothing',
'1 + 1',
'1.01',
'"f"++"a"',
'100 // 2'
'L.range 1 4',
'\\a b -> a + b'
]
).then(function(typesAndValues) { // getTypesAndValues returns the Promise which resolves to array
var types = typesAndValues.types;
var values = typesAndValues.values;
types.forEach(function(type, idx) {
// see ./Types.md for the details on every type definition
console.log('TYPE', Repl.stringify(type), 'VALUE', values[idx]);
});
}).catch(console.error);
Will output:
TYPE (a -> b) -> List a -> List b VALUE <function>
TYPE Maybe.Maybe a VALUE Nothing
TYPE number VALUE 2
TYPE Float VALUE 1.01
TYPE String VALUE "fa"
TYPE List Int VALUE [1,2,3,4]
TYPE Int VALUE 50
TYPE number -> number -> number VALUE <function>
Repl.parseModule
Repl.parseModule
allows you to parse the whole Elm Module and extract not only the types, but also imports and exports. Just point it to your directory with sources, and if you need to parse some nested module, put a dot between the names:
var Repl = require('node-elm-repl');
new Repl({
// the options may vary depending on environment
elmVer: '0.18.0',
workDir: './src'
})
.parseModule('MyModule') // or 'MyLib.MyModule' or 'Sub.Sub.Module'
.then(function(parsedModule) {
console.dir(parsedModule, { depth: null });
});
Will output:
{
"version": {
"major": 0,
"minor": 18,
"patch": 0
},
"package": {
"name": "project",
"user": "user"
},
"imports": [ ... <a-list-of-imports> ... ],
"exports": [ ... <a-list-of-exports> ... ],
"types": [ ... <a-list-of-types> ... ],
...
}
Repl.parseLines
Repl.parseLines
does almost the same as Repl.parseModule
, but creates the module file itself, filling it with the lines you provide and naming it with the name you provide.
It could be used like that:
var Repl = require('node-elm-repl');
new Repl({
// the options may vary depending on environment
elmVer: '0.18.0',
workDir: './src'
})
.parseLines([
"import List",
"",
"myFun : number -> number -> List number",
"myFun a b = ",
" [a] ++ [b]"
], 'MyModule')
.then(function(parsedModule) {
console.dir(parsedModule, { depth: null });
});
The resulting object will have the same structure as the result for Repl.parseModule
above.
Repl.parseModuleText
Repl.parseModuleText
does almost the same as Repl.parseModule
, but takes the module text and creates a file from it:
It could be used like that:
var Repl = require('node-elm-repl');
new Repl({
// the options may vary depending on environment
elmVer: '0.18.0',
workDir: './src'
})
.parseModuleText([
"module MyModule exposing (..)",
"",
"import List",
"",
"myFun : number -> number -> List number",
"myFun a b = ",
" [a] ++ [b]"
].join('\n'))
.then(function(parsedModule) {
console.dir(parsedModule, { depth: null });
});
The resulting object will have the same structure as the result for Repl.parseModule
above.
Repl.stringify
Repl.stringify
is a static method exposed to help you get a friendly version of a Type Definition Tree Structure. You may pass it the nested structure you received from any method described above, i.e.:
new Repl(...).getTypes(...).then(function(types) {
console.log(Repl.stringify(types[0]));
});
// > "number -> number -> List number"
new Repl(...).parseLines(...).then(function(parsedModule) {
console.log(Repl.stringify(parsedModule.types[0].value));
});
// > "number -> number -> List number"
It is also possible to specify a custom specification on how to convert Type Structures to strings, so you may even achieve weird stuff like this:
const mySpec = {
'var': function(name) { return '<' + name + '>'; },
'type': function(name, path) {
return path ? '+ ' + path.join('..') + ' // ' + name + ' +'
: '- ' + name;
},
'aliased': function(name, path) {
return path ? '{{ ' + path.join('^^') + ' --- ' + name + ' }}'
: '[[ ' + name + ' ]]';
},
'lambda': function(left, right) { return right + ' ::: ' + left; },
'app': function(subject, object) { return subject + ' ** ' + object.join('_'); },
'record': function(fields) {
return '& ' + fields.map(function(pair) {
return pair.name + ' >< ' + pair.value;
}).join(' %% ') + ' &';
}
};
new Repl(...).getTypes(...).then(function(types) {
console.log(Repl.stringifyAll(types, mySpec));
});
/* Result:
> Platform ¯\_(ツ)_/¯ Program + ** > Basics ¯\_(ツ)_/¯ Never +_$model$_$msg$ :::
& model GG $model$ %% update GG $model$ ::: $model$ ::: $msg$ %% view GG {{ Html --- Html }} :::
$model$ & ::: < List ** {{ Html --- Html }} ::: {{ Html --- Html }}
> A..B ¯\_(ツ)_/¯ AAA +
*/
See Tests code for more examples.
Repl.stringifyAll
Repl.stringifyAll
is a static method which is actually the shortcut for typesArray.map(Repl.stringify)
:
new Repl(...).getTypes(...).then(Repl.stringifyAll);
// > [ "number -> number -> List number",
// "...",
// "...",
// ...
// ]
new Repl(...).parseLines(...).then(function(parsedModule) {
console.log(
Repl.stringifyAll(
parsedModule.types.map(function(typeSpec) { return typeSpec.value; })
)
);
});
// > [ "number -> number -> List number",
// "...",
// "...",
// ...
// ]
Repl.stringifyAll
accepts custom conversion specifications in the same manner as Repl.stringify
does.
Types
See Types section to discover the detailed stucture of Imports, Exports and Types.
How to use it with CLI?
When you install the package through NPM, CLI should be accessible as node-elm-repl
binary, which is actually an alias for node ./src/cli.js
(or just ./bin/cli
for UNIX environments), so you may safely replace one with another.
Run it without any arguments and you'll observe the detailed help.
Use --from-module
parameter to extract types and values from any Elm module in a form of JSON.
Run ./bin/cli --from-module <your-module-name>
, let's try it with a test sample from this repository:
> ./bin/cli --from-module Anagram --work-dir ./test/samples/elm'
:: : a -> List a -> List a
detect : String -> List String -> List String
isAnagram : String -> String -> Maybe.Maybe String
sortChars : String -> String
> ./bin/cli --from-module Anagram --json --work-dir ./test/samples/elm
{ "package": ..., imports": ..., "exports": ..., "types": ..., "values": ... }
Another way to use it, is to create a file with expressions listed, the ones you need to be evaluated.
If you need imports, list them in the first line starting with ;
and splitting them with ;
.
For example (the contents of src/cli-example
):
;List as L;Maybe exposing ( Maybe(..) );String
L.map
L.foldl
Just
Nothing
1 + 1
\a b -> a + b
Then run node-elm-repl --from <your-file-name>
, node-elm-repl --from ./src/cli-example
in this case.
And you should get:
(a -> b) -> List a -> List b
(a -> b -> b) -> b -> List a -> b
a -> Maybe.Maybe a
Maybe.Maybe a
number
number -> number -> number
Or, if you want to get JSON in response, just add --json
flag.
Repl-CLI accepts different options:
General ones:
--from
— specify a file name to extract all the types, imports and exports from it (intended to be used as a replacement for--from-module
);--from-module
— specify a module name to extract all the types, imports and exports from it (intended to be used as a replacement for--from
);--json
— do not stringify the result, but output to JSON;--work-dir
— specify working directory for execution, for example where the.elm
files you use are located, or where you have yourelm-stuff
andelm-package.json
;--elm-ver
— the exact elm-version you use (default:'0.18.0'
);--user
— your username specified inelm-package.json
(default:'user'
or the one specified inelm-package.json
);--package
— your project specified inelm-package.json
(default:'project'
or the one specified inelm-package.json
);--package-ver
— the version of your project fromelm-package.json
(default:'1.0.0'
);--keep-temp-file
— for debugging purposes, if specified, then do not delete.elm
files after compilation;--keep-elmi-file
— for debugging purposes, if specified, then do not delete.elmi
files after compilation;--show-time
— additionally report the time was spent to extract types, works with--from
only;--module-info
— show module information: imports, exports, when called with--from-module
without--json
flag,--with-values
— include values into the output (takes more time to extract them), works with--from
only;--only-values
— report and extract only values, not the types (overrides--with-values
), works with--from
only;--values-below
— has sense only when--with-values
was used: instead of putting types and values in lines likeTYPE<TAB>VALUE
, put a list of values line-by-line below the list of types: could be useful for parsing, works with--from
only;
How to contribute?
Write a test which fails with npm run test
, file an issue, fork the repository, make a pull request — just any of that or all together will help.