1,004 karma · joined July 4, 2014
I mean, ideally, that'd be great. I don't think there's any fundamental reason things can't be this way, many of these complexities are completely incidental, rather than inherent.
> Is it really too much to ask a newcomer to invest some hours into learning something these days?
No, but when I'm learning something, I prefer for my time to be spent on important stuff, not wasted unnecessarily.
The article mentions:
> remote procedure calls with higher-order functional arguments would require serializing those functions, which is not easy to do safely or efficiently.
This is exactly what Unison is designed to do. Code is not stored as text, but is stored in a serialized AST form and identified by hash. Therefore, transmission between nodes is trivial. The language is still a work in progress, but it's making rapid strides.
Here's an explanation: https://www.youtube.com/watch?v=gCWtkvDQ2ZI
The part relevant to distributed computation is at 30:08, but the earlier parts go through the basics of how code is stored, and why this design choice was made, which might help with understanding what's going on.
If you wanted to go further you could start going the "correct by construction" route - having a data type that enforces more invariants. For example, you might store the recipient name, domain name, and tld of your email in separate fields. Then your parser would more obviously be a parser. I think of the "mere tag" type as a kind of degenerate case of this, rather than something totally separate.
For example, if you have, say,
`getIntFromSomewhere :: IO Int`
and you want to apply a pure function of type `Int -> Int` to it. Supppose the pure function adds one. You might initially think that that's all there is to it, you don't want IO involved in what happens next. But of course, the only reason you would ever do some pure computation is to use the result to have some effect on the world, eg printing it to a console. So actually in a sense, you do want to stay in IO.
myIOAction :: IO ()
myIOAction = do
myInt <- getIntFromSomewhere
print (addOne myInt)
As you say, you can't pull the int out of getIntFromSomewhere without operating in an IO action. But why would we want to, since we obviously want to make use of that value to affect the outside world? And of course, that doesn't mean we have any issues getting that int into a pure function `addOne :: Int -> Int` first, before eventually (immediately, in this case) making use of the result to perform some IO. -- suppose we have some x :: a
fmap g (f x) :: IO [c]
Of course, you wouldn't want the [c] to be able to escape the IO container, because that would break referential transparency - you could no longer guarantee that a function returns the same result given the same arguments. Of course, that's no problem for manipulating the [c] value by passing it to pure code.It's true that "monad's don't compose" in the sense that you need to manually write monad transformers to correctly combine monadic effects, whereas functors compose automatically. However, that's not the same thing as saying it's difficult to use functions which happen to return different monadic results together, if the types match.
Things I love:
- Many common operations are a single keystroke - saving, switching buffers, opening a new buffer. Simple design choices like this make the editor feel incredibly fluid.
- Jump mode is very cool
- Using interactive search for commands and opening files
Things that I miss most coming from vim:
- text objects
- paragraph motion.
- Jumping back to the previous cursor location - often useful after a search.
Overall, I think the decision to neglect almost all motion commands in favor of focusing solely on jump mode, w/b and hjkl, is mistaken. While f is undeniably fast for many operations, there are cases where it performs terribly.
Suppose I want to move a function somewhere else. In vim, I'd probably do something like `{d}` - move to the start of the paragraph, delete til the end of the paragraph.
By contrast, Amp's jump mode kind of implicitly assumes I want to be on the start of some word. It's difficult to quickly jump to an empty line. I have to hit f, enter the character pair for the first target in the paragraph, then hit k. Then I go v, hit f again, go to the final target in the paragraph, press j, and finally hit d. That's a 3 character operation that can be performed with muscle memory, to a 10 character operation that prompts the user for input twice, both times forcing them to slow down to visually locate a point near where they actually want to go. That's a pretty massive useability regression. It'd be good to see some better navigation options for use cases like this.
Pattern matching is often conflated with switch-case by people who are unfamiliar, because pattern matching subsumes switch-case, but they're very much not the same thing. Structural pattern matching is much more powerful than checking a series of boolean conditions. It allows you to not only check the structure of a value, but bind parts of that structure to variables. It allows you to express logic that would would otherwise be fairly complex in a straightforward and transparent way.
fn main() {
// prints "first call worked"
if let Ok(i) = fallible_function(Ok(1)) {
println!("first call worked");
}
// prints "second call failed: FallibleError("error!")"
if let Err(e) = fallible_function(Err("error!".to_string())) {
println!("second call failed: {:?}",e);
}
}
#[derive(Debug)]
struct FallibleError(String);
fn fallible_function(x: Result<i32, String>) -> Result<i32, FallibleError> {
let y = match x {
Err(s) => { return Err(FallibleError(s)); },
Ok(i) => i,
};
// y now contains the i that was in the Ok.
// do straight line code with y here
Ok(y)
}
2. You can create custom errors for a specific function, and put any data that you would have passed to Errorf inside. This way you get the ability to introspect errors to see what went wrong programmatically, and all that data is available for later inspection. Note that we could also have returned a formatted string on error instead of FallibleError exactly like in Go if we wanted to.Of course, the way you'd write fallible_function if you weren't going out of your way to be verbose would be like this:
fn fallible_function(x: Result<i32, String>) -> Result<i32, FallibleError> {
let y = x.map_err(|s| FallibleError(s) )?;
// y now contains the i that was in the Ok.
// do straight line code with y here
Ok(y)
}
Separately, the point of all this is to be able to statically know whether a function can fail or not. We know for a fact that fallible_function can fail. If we write a function fn f(x: i32) -> i32 { .. }
We know for a fact it won't fail (unless it panics, but well behaved code should never panic). We don't even have to worry about the possibility of nils getting in there and screwing us up.