mirror of
https://github.com/roc-lang/roc.git
synced 2025-09-30 07:14:46 +00:00
Move over friendly.md and functional.md
This commit is contained in:
parent
4c06d0357a
commit
a94b1d84f4
4 changed files with 83 additions and 44 deletions
|
@ -1,72 +0,0 @@
|
|||
# Friendly
|
||||
|
||||
Roc prioritizes being a user-friendly language. This impacts the syntax, semantics, and tools Roc ships with.
|
||||
|
||||
## Syntax and source code formatter
|
||||
|
||||
Roc's syntax isn't trivial, but there also isn't much of it to learn. Its design is generally uncluttered and umabiguous. A goal is that you can normally look at a piece of code and quickly get an accurate mental model of what it means, without having to think through several layers of indirection. Here are some examples:
|
||||
|
||||
- `user.email` always means the `email` field of a record named `user`. There's no inheritance, subclassing, or proxying to consider, and this will always do the same thing regardless of what's imported (because there are traits or implicits to consider either).
|
||||
- `Email.isValid` always refers to a constant (probably a function) named `isValid` in a module named `Email`. (Module names are always capitalized, and variables never are.) Modules are always defined statically and can't be modified at runtime; there's no monkeypatching to consider.
|
||||
- `x = combine y z` always declares a new constant `x` (Roc has [no mutable variables, reassignment, or shadowing](/functional)) to be whatever the `combine` function returns when passed the arguments `y` and `z`. (Function calls in Roc don't need parentheses or commas.)
|
||||
- `"My name is \(Str.trim name)"` uses *string interpolation* syntax: a backslash inside a string literal, followed by an expression in parentheses. This code is the same as combining the string `"My name is "` with the string returned by the function call `Str.trim name`. Because Roc's string interpolation syntax begins with a backslash (just like other backlash-escapes such as `\n` and `\"`), you can always tell which parts of a string involve special handling: the parts that begin with backslashes. Everything else works as normal.
|
||||
|
||||
Roc also ships with a source code formatter that helps you maintain a consistent style with little effort. The `roc format` command neatly formats your source code according to a common style, and it's designed with the time-saving feature of having no configuration options. This feature saves you all the time that would otherwise be spent debating which stylistic tweaks to settle on!
|
||||
|
||||
## Helpful compiler
|
||||
|
||||
Roc's compiler is designed to help you out. It does complete type inference across all your code, and the type system is sound. This means you'll never get a runtime type mismatch if everything type-checked (including null exceptions; Roc doesn't have the billion dollar mistake), and you also don't have to write any type annotations for the compiler to be able to infer all the types in your program.
|
||||
|
||||
If there is a problem at compile time, the compiler is designed to report it in a helpful way. Here's an example:
|
||||
|
||||
```
|
||||
── TYPE MISMATCH ────────────────── /home/my-roc-project/main.roc ─
|
||||
|
||||
Something is off with the `then` branch of this `if` expression:
|
||||
|
||||
4│ someInteger : I64
|
||||
5│ someInteger =
|
||||
6│ if someDecimal > 0 then
|
||||
7│ someDecimal + 1
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
This branch is a fraction of type:
|
||||
|
||||
Dec
|
||||
|
||||
But the type annotation on `someInteger` says it should be:
|
||||
|
||||
I64
|
||||
|
||||
Tip: You can convert between integers and fractions using functions like
|
||||
`Num.toFrac` and `Num.round`.
|
||||
```
|
||||
|
||||
If you like, you can run a program that has compile-time errors like this. (If the program reaches the error at runtime, it will crash.) This lets you do things like trying out code that's only partially finished, or running tests for one part of your code base while other parts have compile errors. (Note that this feature is only partially completed, and often errors out; it has a ways to go before it works for all compile errors!)
|
||||
|
||||
## Serialization inference
|
||||
|
||||
- Type inference is used for schema inference, but you can also spell it out if you like
|
||||
- Reports errors immediately
|
||||
|
||||
## Testing
|
||||
|
||||
You can run `roc test` to run all your tests. Each test is declared with the `expect` keyword, and can be as short as one line. For example, this is a complete test:
|
||||
|
||||
```
|
||||
## One plus one should equal two.
|
||||
expect 1 + 1 == 2
|
||||
```
|
||||
|
||||
If the test fails, `roc test` will show you the source code of the `expect`, along with the values of any named variables inside it, so you don't have to separately check what they were. If you write a documentation comment right before it (like `## One plus one should equal two` here), that will also be included in the test output, so you can use that to optionally describe the test if you want to.
|
||||
|
||||
In the future, there are plans to add built-in support for [benchmarking](https://en.wikipedia.org/wiki/Benchmark_(computing)), [generative tests](https://en.wikipedia.org/wiki/Software_testing#Property_testing), [snapshot tests](https://en.wikipedia.org/wiki/Software_testing#Output_comparison_testing), simulated I/O (so you don't have to actually run the real I/O operations, but also don't have to change your code to accommodate the tests), and "reproduction replays"—tests generated from a recording of what actually happened during a particular run of your program, which deterministically simulate all the I/O that happened.
|
||||
|
||||
- also note: future plan to cache tests so we only re-run tests whose answers could possibly have changed. also maybe note: tests that don't perform I/O are guaranteed not to flake b/c pure functions.
|
||||
|
||||
## Future plans
|
||||
- Package manager (Currently just URLs, content-hashed to make them immutable) with searchable index and no installation step, global cache of immutable downloads instead of per-project folders (no need to .gitignore anything)
|
||||
- Step debugger with replay
|
||||
- Customizable "linter" (e.g. code mods, project-specific rules to enforce)
|
||||
- Editor plugin ecosystem that works across editors, where plugins ship with packages
|
||||
- `roc edit`
|
|
@ -1,123 +0,0 @@
|
|||
# Functional
|
||||
|
||||
Roc is designed to have a small number of simple language primitives. This goal leads Roc to be a single-paradigm [functional](https://en.wikipedia.org/wiki/Functional_programming) language, while its [performance goals](/fast) lead to some design choices that are uncommon in functional languages.
|
||||
|
||||
## Opportunistic mutation
|
||||
|
||||
All Roc values are semantically immutable, but may be opportunistically mutated behind the scenes when it would improve performance (without affecting the program's behavior). For example:
|
||||
|
||||
```elm
|
||||
colors
|
||||
|> Set.insert "Purple"
|
||||
|> Set.insert "Orange"
|
||||
|> Set.insert "Blue"
|
||||
```
|
||||
|
||||
The [`Set.insert`](https://www.roc-lang.org/builtins/Set#insert) function takes a `Set` and returns a `Set`. The returned one has the given value inserted into it. Knowing this, it might seem like these three `Set.insert` calls would result in the creation of three brand-new sets, but Roc's *opportunistic mutation* optimizations mean this will be much more efficient than that.
|
||||
|
||||
Opportunistic mutation works by detecting when a semantically immutable value can be safely mutated in-place without changing the behavior of the program. If `colors` is *unique* here—that is, nothing else is currently referencing it, and nothing else will reference it before it goes out of scope—then `Set.insert` will mutate it and then return it. Cloning it first would have no benefit, because nothing in the program could possibly tell the difference!
|
||||
|
||||
If `colors` is not unique, then the first call to `Set.insert` will not mutate it. Instead, it will clone `colors`, insert `"Purple"` into the clone, and then return that. At that point, since the clone will be unique (nothing else is referencing it, since it was just created, and the only thing that will reference it in the future is the `Set.insert` function it's handed off to), the subsequent `Set.insert` calls will all mutate in-place. Roc has ways of detecting uniqueness at compile time, so this optimization will often have no runtime cost, but in some cases it instead uses automatic reference counting to tell when something that was previously shared has become unique over the course of the running program.
|
||||
|
||||
## Everything is immutable (semantically)
|
||||
|
||||
This design means that all Roc values are semantically immutable, even though they can still benefit from the performance of in-place mutation. In many languages, this is reversed; everything is mutable, and it's up to the programmer to "defensively" clone wherever modification is undesirable. Roc's approach means that cloning happens automatically, which can be less error-prone than defensive cloning (which might be accidentally forgotten), but which—to be fair—can also increase unintentional cloning. It's a different default with different tradeoffs.
|
||||
|
||||
A performance benefit of this design compared to having direct mutation primitives is that it lets Roc rule out [reference cycles](https://en.wikipedia.org/wiki/Reference_counting#Dealing_with_reference_cycles). Any language which supports direct mutation can have reference cycles. Detecting these cycles automatically at runtime has a cost (which has similar characteristics to the cost of a tracing garbage collector), and failing to detect them can result in memory leaks. Roc's automatic reference counting neither pays for runtime cycle collection nor memory leaks from cycles, because the language's lack of direct mutation primitives lets it rule out reference cycles at language design time.
|
||||
|
||||
An ergonomics benefit of having no direct mutation primitives is that functions in Roc tend to be chainable by default. For example, consider the `Set.insert` function. In many languages, this function would be written to accept a `Set` to mutate, and then return nothing. In contrast, in Roc it will necessarily be written to return a (potentially) new `Set`, even if in-place mutation will end up happening anyway if it's unique.
|
||||
|
||||
This makes Roc functions naturally amenable to pipelining, as we saw in the earlier example:
|
||||
|
||||
```elm
|
||||
colors
|
||||
|> Set.insert "Purple"
|
||||
|> Set.insert "Orange"
|
||||
|> Set.insert "Blue"
|
||||
```
|
||||
|
||||
To be fair, direct mutation primitives have benefits too. Some algorithms are more concise or otherwise easier to read when written with direct mutation, and direct mutation can make the performance characteristics of some operations clearer.
|
||||
|
||||
As such, Roc's opportunistic mutation design means that reference cycles can be ruled out, and functions will tend to be more ameanable for chaining, but also that some algorithms will be harder to express, and that performance optimization will likely tend to involve more profiling. These tradeoffs fit well with the language's overall design goals.
|
||||
|
||||
## No reassignment or shadowing
|
||||
|
||||
In some languages, the following is allowed.
|
||||
|
||||
```
|
||||
x = 1
|
||||
x = 2
|
||||
```
|
||||
|
||||
In Roc, this will give a compile-time error. Once a name has been assigned to a value, nothing in the same scope can assign it again. (This includes [shadowing](https://en.wikipedia.org/wiki/Variable_shadowing), which is disallowed.) This can make Roc code easier to read, because the answer to the question "might this have a different value at some later point in the scope?" is always "no." That said, this can also make Roc code take longer to write, because of needing to come up with unique names to avoid shadowing, although pipelining (as shown in the previous section) reduces how often intermediate values need to be named.
|
||||
|
||||
A benefit of this design is that it makes Roc code easier to rearrange without causing regressions. Consider this code:
|
||||
|
||||
```elm
|
||||
func = \arg ->
|
||||
greeting = "Hello"
|
||||
welcome = \name -> "\(greeting), \(name)!"
|
||||
…
|
||||
message = welcome "friend"
|
||||
…
|
||||
```
|
||||
|
||||
Suppose I decide to extract the `welcome` function to the top level, so I can reuse it elsewhere:
|
||||
|
||||
```elm
|
||||
func = \arg ->
|
||||
…
|
||||
message = welcome "Hello" "friend"
|
||||
…
|
||||
|
||||
welcome = \prefix, name -> "\(prefix), \(name)!"
|
||||
```
|
||||
|
||||
Without knowing the rest of `func`, we can be confident this change will not alter the code's behavior. In contrast, suppose Roc allowed reassignment. Then it's possible something in the `…` parts of the code could have modified `greeting` before it was used in the `message =` declaration. For example:
|
||||
|
||||
```elm
|
||||
func = \arg ->
|
||||
greeting = "Hello"
|
||||
welcome = \name -> "\(greeting), \(name)!"
|
||||
…
|
||||
if someCondition then
|
||||
greeting = "Hi"
|
||||
…
|
||||
else
|
||||
…
|
||||
…
|
||||
message = welcome "friend"
|
||||
…
|
||||
```
|
||||
|
||||
In this example, if we didn't read the whole function to see that `greeting` was later sometimes (but not always) changed from `"Hello"` to `"Hi"`, we might not have realized that changing it to `message = welcome "Hello" "friend"` would cause a regression due to having the greeting always be `"Hello"`. Because Roc disallows reassignment, this particular regression can't happen, and so the code can be confidently rearranged without checking the rest of the function.
|
||||
|
||||
Even if Roc disallowed reassignment but allowed shadowing, a similar regression could happen if the `welcome` function were shadowed between when it was defined here and when `message` later called it in the same scope. Because Roc allows neither shadowing nor reassignment, these regressions can't happen, and rearranging code can be done with more confidence.
|
||||
|
||||
In fairness, reassignment has benefits too. For example, using it with [early-exit control flow operations](https://en.wikipedia.org/wiki/Control_flow#Early_exit_from_loops) such as a `break` keyword can be a nice way to represent certain types of logic without incurring extra runtime overhead. Roc does not have these operations; looping is done either with convenience functions like [`List.walkUntil`](https://www.roc-lang.org/builtins/List#walkUntil) or with recursion (Roc implements [tail-call optimization](https://en.wikipedia.org/wiki/Tail_call), including [modulo cons](https://en.wikipedia.org/wiki/Tail_call#Tail_recursion_modulo_cons)), but early-exit operators can potentially make some code easier to follow (and potentially even slightly more efficient) when used in scenarios where breaking out of nested loops with a single instruction is desirable.
|
||||
|
||||
## Managed effects over side effects
|
||||
|
||||
Many languages support first-class [asynchronous](https://en.wikipedia.org/wiki/Asynchronous_I/O) effects, which can improve a system's throughput (usually at the cost of some latency) especially in the presence of long-running I/O operations like network requests. Asynchronous effects are commonly represented by a value such as a [Promise or Future](https://en.wikipedia.org/wiki/Futures_and_promises) (Roc calls these Tasks), which represent an effect to be performed. These values can be composed together, potentially while customizing their concurrency properties and supporting I/O interruptions like cancellation and timeouts.
|
||||
|
||||
Most languages also have a separate system for synchronous effects, namely [side effects](https://en.wikipedia.org/wiki/Side_effect_(computer_science)). Having demand for two ways to perform the same effect can lead to a great deal of duplication across a language's ecosystem.
|
||||
|
||||
Instead of having [side effects](https://en.wikipedia.org/wiki/Side_effect_(computer_science)), Roc functions exclusively use *managed effects* in which they return descriptions of effects to run, in the form of Tasks. Tasks can be composed and chained together, until they are ultimately handed off (usually via a `main` function or something similar) to an effect runner outside the program, which actually performs the effects the tasks describe.
|
||||
|
||||
Having only (potentially asynchronous) *managed effects* and no (synchronous) *side effects* both simplifies the language's ecosystem and makes certain guarantees possible. For example, the combination of managed effects and semantically immutable values means all Roc functions are [pure](https://en.wikipedia.org/wiki/Pure_function)—that is, they have no side effects and always return the same answer when called with the same arguments.
|
||||
|
||||
## Pure functions
|
||||
|
||||
Pure functions have a number of implementation benefits, such as [referential transparency](https://en.wikipedia.org/wiki/Referential_transparency) and being trivial to [memoize](https://en.wikipedia.org/wiki/Memoization). They also have testing benefits; for example, Roc tests which either use simulated effects (or which do not involve tasks at all) never flake. They either consistently pass or consistently fail. Because of this, their results can be cached, so `roc test` can skip re-running them unless their source code (including dependencies) changed. (This caching has not yet been implemented, but is planned.)
|
||||
|
||||
Roc does support [tracing](https://en.wikipedia.org/wiki/Tracing_(software)) via the `dbg` keyword, an essential [debugging](https://en.wikipedia.org/wiki/Debugging) tool which is unusual among side effects in that—similarly to opportunistic mutation—using it should not affect the behavior of the program. As such, it usually does not impact the guarantees of pure functions aside from potentially impeding optimizations. (An example of an exception to this would be if a program sent its `dbg` traces to log files which the program itself then read back in. This is not recommended.)
|
||||
|
||||
Pure functions are notably amenable to compiler optimizations. Roc already takes advantage of them to implement [function-level dead code elimination](https://elm-lang.org/news/small-assets-without-the-headache). Here are some other examples of optimizations that will benefit from this in the future; these are planned, but not yet implemented:
|
||||
|
||||
- [Loop fusion](https://en.wikipedia.org/wiki/Loop_fission_and_fusion), which can do things like combining consecutive `List.map` calls (potentially intermingled with other operations that traverse the list) into one pass over the list.
|
||||
- [Compile-time evaluation](https://en.wikipedia.org/wiki/Compile-time_function_execution), which basically takes [constant folding](https://en.wikipedia.org/wiki/Constant_folding) to its natural limit: anything that can be evaluated at compile time is evaluated then. This saves work at runtime, and is easy to opt out of: if you want evaluation to happen at runtime, you can instead wrap the logic in a function and call it as needed.
|
||||
- [Hoisting](https://en.wikipedia.org/wiki/Loop-invariant_code_motion), which moves certain operations outside loops to prevent them from being re-evaluated unnecessarily on each step of the loop. It's always safe to hoist calls to pure functions, and in some cases they can be hoisted all the way to the top level, at which point they become eligible for compile-time evaluation.
|
||||
|
||||
## Get started
|
||||
|
||||
If this design sounds interesting to you, you can give Roc a try by heading over to the [tutorial](https://www.roc-lang.org/tutorial)!
|
Loading…
Add table
Add a link
Reference in a new issue