The API is really good, clean and simple. Two questions:
Does callJs
allow for (almost) arbitrary code execution?
Do you have any plans for file I/O or http servers?
The API is really good, clean and simple. Two questions:
Does callJs
allow for (almost) arbitrary code execution?
Do you have any plans for file I/O or http servers?
I think the module shouldnāt be named āPosixā anymore. Only IO or something similar. And then āIO.Posixā could have convenience functions for working with Posix APIs.
I also think that this pattern is also useful for usage in the browser. It reminds me of elm-porter or elm-procedure. The only thing Iām missing from them is some kind of running process management. But that might be a topic for another post.
I think itās a crucial missing feature of elm, absence of meaningful CLI. Imagine you teach a class using elm (well, I tried few months ago, it was not much fun, even though students had prior Haskell exposure, not the least due to absence of CLI, so the learning curve was too steep for many).
Given it is a FE language, I think any lessons should be HTML
focused, not cli
focused
But I miss the opportunity to write simple scripts with Elm as well, so Iām really looking forward for this. Great idea @albertdahlin
I have been using Elm for teaching FP (and visualization) for a few years now, and use litvis for literate Elm without having to confuse students with TEA or html.
One thing Iāve missed though is the ability for direct file IO and it looks like elm-posix does exactly the job, so really helpful.
I taught computer graphics (and will do next year as well), and itās not only FE. Sometimes Iād like to compute, say, a vector in 3d - FE has nothing to do with it, it just gets in the way.
This is really cool, I think elm while being frontend focused should still have support for clis scripts so that the community can make frontend tooling in elm itself. That would really drive the tooling ecosystem forward.
I found the andThen pattern to be quite readable and avoids those wildcard parameters:
module HelloFile exposing (program)
import Posix.IO as IO exposing (IO, Process)
import Posix.IO.File as File
import Posix.IO.Process as Proc
program : Process -> IO ()
program process =
case process.argv of
[ _, inName ] ->
-- Print to stdout if no output file provided
File.contentsOf inName
|> IO.exitOnError identity
|> IO.andThen (processContent >> Proc.print)
[ _, inName, outName ] ->
File.contentsOf inName
|> IO.exitOnError identity
|> IO.andThen (processContent >> File.writeContentsTo outName)
_ ->
Proc.logErr "Error: Provide the names of the input file and optionally an output file.\n"
processContent : String -> String
processContent =
-- Do something with the content of the input file here.
String.toUpper
I have been working on something that could use this. Currently Iām using ports and Platform.worker
called from a NodeJS script.
My idea for this package is to make it more ergonomic to write simple CLI scripts, for example dev tools and build pipelines. I also want to make it as open as possible to allow others to implement their own I/O in nodejs. Supporting HTTP-servers is however out of scope for now.
Iām currently working on specifying the API for the following I/O modules:
My plan for callJs is to simplify user specific I/O implementations and allow people to experiment.
CallJs is just a wrapper for communicating with the āoutsideā using two ports.
To create your own project specific I/O implementation my current thinking goes along these lines:
Create a standard nodejs module that exports an Object with all your I/O functions, e.g
js/my-functions.js
module.exports = {
addOne: function(num) {
this.send(num + 1); // this.send is a normal Elm port
},
}
Then create an Elm module where you supply the Encoder / Decoder for communicating with Javascript, e.g src/MyModule.elm
addOne : Int -> IO x Int
addOne n =
IO.callJs
"addOne"
[ Encode.int n
]
Decode.int
program : IO.Process -> IO String ()
program _ =
...
When you run or compile you can add arguments to supply your I/O implementation, something like this:
elm.cli run --extend-io js/my-functions.js src/MyModule.elm
My idea for this package is to make it more ergonomic to write simple CLI scripts, for example dev tools and build pipelines. I also want to make it as open as possible to allow others to implement their own I/O in nodejs.
Cool!
Supporting HTTP-servers is however out of scope for now.
Fair
While the API for reading and writing a whole file at once can work for simple cases, having a streaming API is definitely something one eventually wants. On the other hand the 1.0 API you published has the obvious problem (that I guess youāre aware of) wrt writing to closed files.
Ideas:
One could imagine a withFile : Filename -> (FD x -> IO e a) -> IO e a
API that automatically closes the file at the end, BUT the problem is that you would be able to āsmuggle outā the file descriptor via return
.
What if we donāt expose the file descriptor, but instead withFile : Filename -> ({read : Int -> IO e String, write : String -> IO e ()} -> IO e a) -> IO e a
? Again, you can just return
the read
and andThen
it out, so doesnāt work.
We could force a
to be ()
and then one wouldnāt be able to smuggle out anything, but you could only use that for writing, not for readingā¦ not great.
We could have an opaque type FileIO e a = FileIO (IO e a)
. Then withFile : Filename -> FileIO e a -> IO e a
and then read : FileIO Err String
, write : String -> FileIO Err ()
+ monad operations for FileIO
but then you canāt allow nesting withFile
calls because we either get back to the smuggling problem or you have an API where only the inner file can be read/written to inside the nested call. Not flexible enough I think.
Uhm. You could restrict the return to be an IO Value
where Value
is the Json.Encode
one butā¦ meh, itās unclean.
Iāll think a bit more about this.
Why not have the functions return the value (instead of calling send)? You avoid both double-send and no-send that way
Great!
Simple, I like it!
I think the ideas you are writing about are interesting, thank you.
I want to be pragmatic in the design of the API. The āgut feelingsā I have been basing my decisions on are something like this:
I had that in the beginning but then I wanted to do async stuff, like
sleep: function(delay) {
setTimeout(this.send, delay);
}
so I went with the more flexible approach of using callbacks. My thinking was that increased flexibility outweighs the increased risk. Iām open to other opinions or suggestions here though, maybe it is better to revert to returning values.
Is it maybe a better idea to have the functions return either a value or a Promise
to allow both styles?
Yupp
I mean, most errors are not avoidable at the API level: write errors, permission errors, full filesystem, etc etc etc, can and need to be managed via Result
/IO err
, as you already do.
Writing to a closed fd feels like one of those problems that are be fixable with a Strict Enough API but:
withFile : (FD -> IO e a) -> IO e a
Something something records keeping tracks of open files via type level shenanigans, elm-css-ish style.
I think allowing either a value or a Promise
should be flexible enough yet clean and avoid the issue.
Hey @albertdahlin,
I have finally get to try your elm-posix
out. I have tried the HelloUser
example got two problems. First problem was when I have tried to run it I have got:
You found a bug!
Please report at https://github.com:albertdahlin/elm-posix/issues
Copy the information below into the issue:
IO Function "sleep" not implemented.
and second one was when I have tried to make and run the compiled version which gave me:
test.js:3518
fn.apply(app.ports.recv, msg.args);
^
TypeError: Cannot read property 'apply' of undefined
at Array.<anonymous> (/Users/tomas.latall/test.js:3518:8)
at Function.f (/Users/tomas.latall/test.js:2228:19)
at A3 (/Users/tomas.latall/test.js:68:28)
at Object.b (/Users/tomas.latall/test.js:1987:7)
any hints what I might have missed?
It looks like the Elm package and the npm package are different versions. Try updating both, latest version is 1.0.2
.
Thanks to everyone who have provided feedback and ideas so far. This inspired me to working on an improved version of this package. It now includes
copy
, mkdir
etc.This is more or less the feature set I have planned to include for now. Is there anything crucial I have missed?
Next step is to implement everything, polish the API and improve the documentation. Feedback and ideas to improve the API is very welcome and appreciated.
An idea rather than a proposal: why not move everything to IO
instead of Posix.IO
?
Task
interop API is good.andThen
recursively, but I guess it would run out of stack relatively quickly. Not even sure what an API for a loop would look like though to be honest).WriteMode
is So Much Better than the 'a/w/w+/zomg` APIwrite_
being able to return a ReadError
, API-wise, but I think I can see why it makes sense to avoid complicating typesToManyFilesOpen
ā TooManyFilesOpen
openWriteStream
has maybe the wrong type?exec
: I can see why you called it this, but exec
has a specific meaning in Posix (replace currently running code with given application), this is more like a system
. You may call is spawn
?kill
: maybe also allow to send signal different than SIGTERM/SIGKILL?send
: is this a āwrite into program stdinā function? if so, the doc is not completely clearbytes
: please specify the encoding usedgzip
/gunzip
: strongly consider Stream Bytes Bytes
instead, it could be compressed binary dataread
: āsize represents different thingsāā¦ uhmā¦ Iām not in love with this. I can see it making sense, but I donāt love it. I guess adding the āsize typeā to Stream
would make all the types longer and more annoying but stillā¦
write
: the documentation is incomplete, it can also write bytesrun
āisā ā āareāpipeTo
: I really like this API. I really love the whole Streams
API in general tbh.toStream : (a -> b) -> Stream a b
?read_
and write_
with typed errors I think. In particular, what if I want to read until EOF, but not use run
?Thank you very much for the feedback. This was really helpful.
Fixed above
I have added read_
and write_
to the stream module. I will continue to work through the Error types.
I tested to do a recursive forever loop with andThen
and it does not blow up the stack in examples/src/Forever.elm. There are for sure cases where the stack will blow up but I would say this is good enough for now.
Maybe this is better?
type ReadError
= CouldNotOpenForRead OpenError
| ...
type WriteError
= CouldNotOpenForWrite OpenError
| ...
The current Error
type made more sense when I had openReadWrite
.
exec
, execFile
and spawn
are taken directly from the nodejs child process modulesend
and receive
functions and have spawn returning streams for stdIO instead.I agree, Iām not thrilled about this solution either. Maybe we can do better?
How about something like this?
read : Stream x output -> IO String output
chunkBytes : Int -> Stream Bytes Bytes
chunkString : Int -> Stream String String
{-| Will read at most 10 bytes from stdIn each time. Might read less
if source stream is exhausted (EOF is reached). -}
read10BytesFromStdIn : IO String Bytes
read10BytesFromStdIn =
stdIn
|> pipeTo (chunkBytes 10)
|> read
{-| Since no "chunker" is applied this will read until EOF. -}
readEverythingAtOnce : IO String Bytes
readEverythingAtOnce =
stdIn
|> read
I think this is a better API
I think this is a bit more involved since it would require handling buffer or generator states in most cases so I decided to skip it for now.
Consider these examples:
naturalNumbers : Stream Never Int
tuple : Stream a ( a, a )
split : Stream ( a, a ) a
I will consider this when the Iām done specifying the API.