The original motivation for adding Record
to x7
is the ability to open, read, and write to files. We'll back the x7
File implementation by the rust
File struct, so let's make a new file in x7
- records/file.rs
:
We will start by making a FileRecord
struct:
#[derive(Clone, Debug)] pub(crate) struct FileRecord { path: String, // The Record trait requires Sync + Send file: Arc<Mutex<std::fs::File>>, }
The type Arc<Mutex<std::fs::File>>
is necessary as x7
requires all types to be thread safe.
Now that we have a struct, let's expose a way to generate one from x7
. We want the following x7
expression to work:
This will map to a Expr::String("file-name")
in the interpreter, so we need two methods:
- A way to open files given a
String
- A way to open files given an
Expr::String
With that in mind, here's the two relevant methods:
impl FileRecord { /// Open a file with the given Path pub(crate) fn open_file(path: String) -> LispResult<Expr> { // Open the file with liberal permissions. let f = OpenOptions::new() .write(true) .create(true) .read(true) .open(path.clone()) .map_err(|e| anyhow!("Could not open file \"{}\" because {}", &path, e))?; // Make the path pretty. let abs_path = fs::canonicalize(path) .map_err(|e| anyhow!("Could not canonicalize path! {}", e))? .to_str() .ok_or_else(|| anyhow!("Could not represent path as UTF-8 string"))? .into(); // record! is a macro to assist in making LispResult<Expr::Record> types record!(FileRecord::new(f, abs_path)) } /// Open a file from x7 /// This function signature will let us expose it directly to the interpreter pub(crate) fn from_x7(exprs: Vector<Expr>, _symbol_table: &SymbolTable) -> LispResult<Expr> { exact_len!(exprs, 1); let path = exprs[0].get_string()?; FileRecord::open_file(path) } }
Now that we have the ability to make a FileRecord
, we'll need to implement Record
so it can be understood by the interpreter (Expr::Record
).
impl Record for FileRecord { fn call_method(&self, sym: &str, args: Vector<Expr>) -> LispResult<Expr> { // We have no methods yet. unknown_method!(self, sym) } fn type_name(&self) -> &'static str { "FileRecord" } fn display(&self) -> String { format!("File<{}>", self.path) } fn debug(&self) -> String { self.display() } fn clone(&self) -> RecordType { Box::new(Clone::clone(self)) } fn methods(&self) -> Vec<&'static str> { Vec::new() } fn id(&self) -> u64 { use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; let mut h = DefaultHasher::new(); self.path.hash(&mut h); h.finish() } }
We also need to expose FileRecord::from_x7
to the interpreter, so let's head back and add it to make_stdlib_fns
:
make_stdlib_fns!{ // elided functions... ("call_method", 2, call_method, true, "<doc-string>"), // Open a file ("fs::open", 1, FileRecord::from_x7, true, "Open a file."), }
We can now compile and run x7
to see what happens:
>>> (def f (fs::open "hello-world.txt")) nil >>> f File</home/david/programming/x7/hello-world.txt>
Nice! We've opened a file. We can now implement some other useful methods on FileRecord
like reading from a file:
impl FileRecord { /// Read the contents of a file to a String, /// rewinding the cursor to the front. fn read_all(&self) -> LispResult<String> { let mut buf = String::new(); let mut guard = self.file.lock(); guard .read_to_string(&mut buf) .map_err(|e| anyhow!("Failed to read to string {}", e))?; rewind_file!(guard); Ok(buf) } /// Read the contents of a FileRecord to a string. fn read_to_string(&self, args: Vector<Expr>) -> LispResult<Expr> { // We want no arguments. exact_len!(args, 0); self.read_all().map(Expr::String) } }
We can update our Record
implementation for FileRecord
to include this method:
impl Record for FileRecord { fn call_method(&self, sym: &str, args: Vector<Expr>) -> LispResult<Expr> { match sym { "read_to_string" => self.read_to_string(args), _ => unknown_method!(self, sym), } } }
And use it:
~ echo "hello" > hello-world.txt ~ x7 >>> (def f (fs::open "hello-world.txt")) >>> (call_method f "read_to_string") "hello"
Awesome! We're able to call methods on FileRecord
. It's the same process to implement .write
and other useful file operations, so we'll elide it. This is great stuff, and would be even better with some syntactic sugar.
Let's add method call syntax so these two expressions are equal:
>>> (call_method f "read_to_string") >>> (.read_to_string f)
from Hacker News https://ift.tt/3c5RqKZ
No comments:
Post a Comment
Note: Only a member of this blog may post a comment.