diff --git a/concepts/use-expressions/.meta/config.json b/concepts/use-expressions/.meta/config.json new file mode 100644 index 000000000..457fb18ae --- /dev/null +++ b/concepts/use-expressions/.meta/config.json @@ -0,0 +1,8 @@ +{ + "blurb": "Gleam's use expression syntax makes working with nested callbacks easier and more readable.", + "authors": [ + "lpil" + ], + "contributors": [ + ] +} diff --git a/concepts/use-expressions/about.md b/concepts/use-expressions/about.md new file mode 100644 index 000000000..af05c8658 --- /dev/null +++ b/concepts/use-expressions/about.md @@ -0,0 +1,70 @@ +# About + +In Gleam it is common to write and use higher order functions, that is functions that take other functions as arguments. Sometimes when using many higher order functions at once the code can become difficult to read, with many layers of indentation. + +For example, here is a function that calls several functions that return `Result(Int, Nil)`, and sums the values if all four are successful. + +```gleam +import gleam/result + +pub fn main() -> Result(Int, Nil) { + result.try(function1(), fn(a) { + result.try(function2(), fn(b) { + result.try(function3(), fn(c) { + result.try(function4(), fn(d) { + Ok(a + b + c + d) + }) + }) + }) + }) +} +``` + +Gleam's `use` expressions allow us to write this code without the indentation, often making it easier to read. + +```gleam +import gleam/result + +pub fn main() -> Result(Int, Nil) { + use a <- result.try(function1()) + use b <- result.try(function2()) + use c <- result.try(function3()) + use d <- result.try(function4()) + Ok(a + b + c + d) +} +``` + +A `use` expression collects all the following statements in the block and passes it as a callback function as the final argument to the function call. The variables between the `use` keyword and the `<-` symbol are the names of the arguments that will be passed to the callback function. + +```gleam +// This use expression +use a <- function(1, 2) +io.println("Hello!") +a + +// Is equivalent to this normal function call +function(1, 2, fn(a) { + io.println("Hello!") + a +}) +``` + +The callback function can take any number of arguments, or none at all. + +```gleam +use a, b, c, d <- call_4_function() + +use <- call_0_function() +``` + +There are no special requirements to create a function that can be called with a `use` expression, other than taking a callback function as the final argument. + +```gleam +pub fn call_twice(function: fn() -> t) -> #(t, t) { + let first = function() + let second = function() + #(first, second) +} +``` + +Gleam's `use` expressions are a very powerful feature that can be applied to lots of problems, but when overused they can make code difficult to read. It is generally preferred to use the normal function call syntax and only reach for `use` expressions when they make the code easier to read. diff --git a/concepts/use-expressions/introduction.md b/concepts/use-expressions/introduction.md new file mode 100644 index 000000000..87050692f --- /dev/null +++ b/concepts/use-expressions/introduction.md @@ -0,0 +1,70 @@ +# Introduction + +In Gleam it is common to write and use higher order functions, that is functions that take other functions as arguments. Sometimes when using many higher order functions at once the code can become difficult to read, with many layers of indentation. + +For example, here is a function that calls several functions that return `Result(Int, Nil)`, and sums the values if all four are successful. + +```gleam +import gleam/result + +pub fn main() -> Result(Int, Nil) { + result.try(function1(), fn(a) { + result.try(function2(), fn(b) { + result.try(function3(), fn(c) { + result.try(function4(), fn(d) { + Ok(a + b + c + d) + }) + }) + }) + }) +} +``` + +Gleam's `use` expressions allow us to write this code without the indentation, often making it easier to read. + +```gleam +import gleam/result + +pub fn main() -> Result(Int, Nil) { + use a <- result.try(function1()) + use b <- result.try(function2()) + use c <- result.try(function3()) + use d <- result.try(function4()) + Ok(a + b + c + d) +} +``` + +A `use` expression collects all the following statements in the block and passes it as a callback function as the final argument to the function call. The variables between the `use` keyword and the `<-` symbol are the names of the arguments that will be passed to the callback function. + +```gleam +// This use expression +use a <- function(1, 2) +io.println("Hello!") +a + +// Is equivalent to this normal function call +function(1, 2, fn(a) { + io.println("Hello!") + a +}) +``` + +The callback function can take any number of arguments, or none at all. + +```gleam +use a, b, c, d <- call_4_function() + +use <- call_0_function() +``` + +There are no special requirements to create a function that can be called with a `use` expression, other than taking a callback function as the final argument. + +```gleam +pub fn call_twice(function: fn() -> t) -> #(t, t) { + let first = function() + let second = function() + #(first, second) +} +``` + +Gleam's `use` expressions are a very powerful feature that can be applied to lots of problems, but when overused they can make code difficult to read. It is generally preferred to use the normal function call syntax and only reach for `use` expressions when they make the code easier to read. diff --git a/concepts/use-expressions/links.json b/concepts/use-expressions/links.json new file mode 100644 index 000000000..0d4f101c7 --- /dev/null +++ b/concepts/use-expressions/links.json @@ -0,0 +1,2 @@ +[ +] diff --git a/config.json b/config.json index 116a05034..0c6833a89 100644 --- a/config.json +++ b/config.json @@ -435,6 +435,19 @@ "maps" ], "status": "active" + }, + { + "slug": "expert-experiments", + "name": "Expert Experiments", + "uuid": "481133be-f550-4639-8e26-4f03fe292d1c", + "concepts": [ + "use-expressions" + ], + "prerequisites": [ + "results", + "anonymous-functions" + ], + "status": "active" } ], "practice": [ @@ -1999,6 +2012,11 @@ "uuid": "6b3a8671-4fef-4e72-ab58-2fc12893d9ae", "slug": "external-types", "name": "External Types" + }, + { + "uuid": "e33cc76d-a5c5-47cb-ab91-aa1f2da988b9", + "slug": "use-expressions", + "name": "Use Expressions" } ], "key_features": [ diff --git a/exercises/concept/expert-experiments/.docs/hints.md b/exercises/concept/expert-experiments/.docs/hints.md new file mode 100644 index 000000000..75e391245 --- /dev/null +++ b/exercises/concept/expert-experiments/.docs/hints.md @@ -0,0 +1,15 @@ +# Hints + +## 1. Define the `with_retry` function + +- A `case` expression can be used to pattern match on a result. + +## 2. Define the `record_timing` function + +- The `time_logger` function should be called even if the `experiment` function returns an `Error` value. + +## 3. Define the `run_experiment` function + +- The [`result.try` function][result-try] can be used in a `use` expression to stop if a result is an `Error` value. + +[result-try]: https://hexdocs.pm/gleam_stdlib/gleam/result.html#try diff --git a/exercises/concept/expert-experiments/.docs/instructions.md b/exercises/concept/expert-experiments/.docs/instructions.md new file mode 100644 index 000000000..f76659106 --- /dev/null +++ b/exercises/concept/expert-experiments/.docs/instructions.md @@ -0,0 +1,64 @@ +# Instructions + +Daphne has been working on a system to run and record the results of her experiments. Some of the code has become a bit verbose and repetitive, so she's asked you to write some `use` expressions to help clean it up. + +## 1. Define the `with_retry` function + +Sometimes experiments can fail due to a one-off mistake, so if an experiment fails Daphne wants to retry it again to see if it works the second time. + +Define the `with_retry` function that takes a result returning function as an argument. + +If the function returns an `Ok` value then `with_retry` should return that value. + +If the function returns an `Error` value then `with_retry` should call the function again and return the result of that call. + +Daphne will use the function like this: + +```gleam +pub fn main() { + use <- with_retry + // Perform the experiment here +} +``` + +## 2. Define the `record_timing` function + +Daphne records how long each experiment takes to run by calling a time logging function before and after each experiment. + +Define the `record_timing` function that takes two arguments: +- A time logging function which takes no arguments and returns `Nil`. +- An experiment function which takes no arguments and returns a result. + +`record_timing` should call the time logging function, then call the experiment function, then call the time logging function again, and finally return the result of the experiment function. + +Daphne will use the function like this: + +```gleam +pub fn main() { + use <- record_timing(time_logger) + // Perform the experiment here +} +``` + +## 3. Define the `run_experiment` function + +Experiments are made up of three phases. The setup, the action, and the recording. All three phases return results, and each phase needs the successful result of the previous phase to run. + +Define the `run_experiment` function that takes four arguments: +- The name of the experiment as a `String`. +- A setup function which takes no arguments and returns a result. +- An action function which takes the `Ok` value of the setup function as an argument and returns a result. +- A recording function which takes the `Ok` value of the setup and functions as an arguments and returns a result. + +If all three functions succeed then `run_experiment` should return `Ok(#(experiment_name, recording_data))`. + +If any of the functions return an `Error` value then `run_experiment` should return that value. + +Daphne will use the function like this: + +```gleam +pub fn main() { + use setup_data, action_data <- run_experiment("Test 1", setup, action) + // Record the results here +} +``` diff --git a/exercises/concept/expert-experiments/.docs/introduction.md b/exercises/concept/expert-experiments/.docs/introduction.md new file mode 100644 index 000000000..d39967a6a --- /dev/null +++ b/exercises/concept/expert-experiments/.docs/introduction.md @@ -0,0 +1,72 @@ +# Introduction + +## Use Expressions + +In Gleam it is common to write and use higher order functions, that is functions that take other functions as arguments. Sometimes when using many higher order functions at once the code can become difficult to read, with many layers of indentation. + +For example, here is a function that calls several functions that return `Result(Int, Nil)`, and sums the values if all four are successful. + +```gleam +import gleam/result + +pub fn main() -> Result(Int, Nil) { + result.try(function1(), fn(a) { + result.try(function2(), fn(b) { + result.try(function3(), fn(c) { + result.try(function4(), fn(d) { + Ok(a + b + c + d) + }) + }) + }) + }) +} +``` + +Gleam's `use` expressions allow us to write this code without the indentation, often making it easier to read. + +```gleam +import gleam/result + +pub fn main() -> Result(Int, Nil) { + use a <- result.try(function1()) + use b <- result.try(function2()) + use c <- result.try(function3()) + use d <- result.try(function4()) + Ok(a + b + c + d) +} +``` + +A `use` expression collects all the following statements in the block and passes it as a callback function as the final argument to the function call. The variables between the `use` keyword and the `<-` symbol are the names of the arguments that will be passed to the callback function. + +```gleam +// This use expression +use a <- function(1, 2) +io.println("Hello!") +a + +// Is equivalent to this normal function call +function(1, 2, fn(a) { + io.println("Hello!") + a +}) +``` + +The callback function can take any number of arguments, or none at all. + +```gleam +use a, b, c, d <- call_4_function() + +use <- call_0_function() +``` + +There are no special requirements to create a function that can be called with a `use` expression, other than taking a callback function as the final argument. + +```gleam +pub fn call_twice(function: fn() -> t) -> #(t, t) { + let first = function() + let second = function() + #(first, second) +} +``` + +Gleam's `use` expressions are a very powerful feature that can be applied to lots of problems, but when overused they can make code difficult to read. It is generally preferred to use the normal function call syntax and only reach for `use` expressions when they make the code easier to read. diff --git a/exercises/concept/expert-experiments/.docs/introduction.md.tpl b/exercises/concept/expert-experiments/.docs/introduction.md.tpl new file mode 100644 index 000000000..f7ad76b57 --- /dev/null +++ b/exercises/concept/expert-experiments/.docs/introduction.md.tpl @@ -0,0 +1,3 @@ +# Introduction + +%{concept:use-expressions} diff --git a/exercises/concept/expert-experiments/.gitignore b/exercises/concept/expert-experiments/.gitignore new file mode 100644 index 000000000..170cca981 --- /dev/null +++ b/exercises/concept/expert-experiments/.gitignore @@ -0,0 +1,4 @@ +*.beam +*.ez +build +erl_crash.dump diff --git a/exercises/concept/expert-experiments/.meta/config.json b/exercises/concept/expert-experiments/.meta/config.json new file mode 100644 index 000000000..c763051b8 --- /dev/null +++ b/exercises/concept/expert-experiments/.meta/config.json @@ -0,0 +1,25 @@ +{ + "authors": [ + "lpil" + ], + "contributors": [ + ], + "files": { + "solution": [ + "src/expert_experiments.gleam" + ], + "test": [ + "test/expert_experiments_test.gleam" + ], + "exemplar": [ + ".meta/example.gleam" + ], + "invalidator": [ + "gleam.toml", + "manifest.toml" + ] + }, + "forked_from": [ + ], + "blurb": "Learn about use expressions by conducting some experiments" +} diff --git a/exercises/concept/expert-experiments/.meta/design.md b/exercises/concept/expert-experiments/.meta/design.md new file mode 100644 index 000000000..7a3f2ff05 --- /dev/null +++ b/exercises/concept/expert-experiments/.meta/design.md @@ -0,0 +1,29 @@ +# Design + +## Learning objectives + +- Know about use expressions. +- Know they can be used to avoid indentation. +- Know they can be used with any function that takes a function as a final argument. + +## Out of scope + +- Use expressions are block scoped, not function scoped like similar constructs in other languages. +- Use with JavaScript promises. + +## Concepts + +- `use-expressions` + +## Prerequisites + +- `results` +- `anonymous-functions` + +## Analyzer + +This exercise could benefit from the following rules added to the [analyzer][analyzer]: + +- Verify that the `run_experiment` function uses `use` expressions. + +[analyzer]: https://github.com/exercism/gleam-analyzer diff --git a/exercises/concept/expert-experiments/.meta/example.gleam b/exercises/concept/expert-experiments/.meta/example.gleam new file mode 100644 index 000000000..11adb0b01 --- /dev/null +++ b/exercises/concept/expert-experiments/.meta/example.gleam @@ -0,0 +1,28 @@ +import gleam/result + +pub fn with_retry(experiment: fn() -> Result(t, e)) -> Result(t, e) { + experiment() + |> result.lazy_or(experiment) +} + +pub fn record_timing( + time_logger: fn() -> Nil, + experiment: fn() -> Result(t, e), +) -> Result(t, e) { + time_logger() + let result = experiment() + time_logger() + result +} + +pub fn run_experiment( + name: String, + setup: fn() -> Result(t, e), + action: fn(t) -> Result(u, e), + record: fn(t, u) -> Result(v, e), +) -> Result(#(String, v), e) { + use setup_data <- result.try(setup()) + use action_data <- result.try(action(setup_data)) + use record_data <- result.try(record(setup_data, action_data)) + Ok(#(name, record_data)) +} diff --git a/exercises/concept/expert-experiments/gleam.toml b/exercises/concept/expert-experiments/gleam.toml new file mode 100644 index 000000000..6d44caa88 --- /dev/null +++ b/exercises/concept/expert-experiments/gleam.toml @@ -0,0 +1,11 @@ +name = "expert_experiments" +version = "0.1.0" + +[dependencies] +gleam_bitwise = "~> 1.2" +gleam_otp = "~> 0.7" +gleam_stdlib = "~> 0.30" +simplifile = "~> 0.1" + +[dev-dependencies] +exercism_test_runner = "~> 1.4" diff --git a/exercises/concept/expert-experiments/manifest.toml b/exercises/concept/expert-experiments/manifest.toml new file mode 100644 index 000000000..a8cdf8b1d --- /dev/null +++ b/exercises/concept/expert-experiments/manifest.toml @@ -0,0 +1,25 @@ +# This file was generated by Gleam +# You typically do not need to edit this file + +packages = [ + { name = "exercism_test_runner", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "glance", "gleam_json", "gleam_community_ansi", "gleam_stdlib", "simplifile", "gap"], otp_app = "exercism_test_runner", source = "hex", outer_checksum = "336FBF790841C2DC25EB77B35E76A09EFDB9771D7D813E0FDBC71A50CB79711D" }, + { name = "gap", version = "0.7.0", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_stdlib"], otp_app = "gap", source = "hex", outer_checksum = "AF290C27B3FAE5FE64E1B7E9C70A9E29AA0F42429C0592D375770C1C51B79D36" }, + { name = "glance", version = "0.7.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "glexer"], otp_app = "glance", source = "hex", outer_checksum = "B646A08970990D9D7A103443C5CD46F9D4297BF05F188767777FCC14ADF395EA" }, + { name = "gleam_bitwise", version = "1.3.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_bitwise", source = "hex", outer_checksum = "E2A46EE42E5E9110DAD67E0F71E7358CBE54D5EC22C526DD48CBBA3223025792" }, + { name = "gleam_community_ansi", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_bitwise", "gleam_community_colour"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "6E4E0CF2B207C1A7FCD3C21AA43514D67BC7004F21F82045CDCCE6C727A14862" }, + { name = "gleam_community_colour", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_bitwise"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "D27CE357ECB343929A8CEC3FBA0B499943A47F0EE1F589EE16AFC2DC21C61E5B" }, + { name = "gleam_erlang", version = "0.22.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "367D8B41A7A86809928ED1E7E55BFD0D46D7C4CF473440190F324AFA347109B4" }, + { name = "gleam_json", version = "0.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "C6CC5BEECA525117E97D0905013AB3F8836537455645DDDD10FE31A511B195EF" }, + { name = "gleam_otp", version = "0.7.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "ED7381E90636E18F5697FD7956EECCA635A3B65538DC2BE2D91A38E61DCE8903" }, + { name = "gleam_stdlib", version = "0.31.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "6D1BC5B4D4179B9FEE866B1E69FE180AC2CE485AD90047C0B32B2CA984052736" }, + { name = "glexer", version = "0.6.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glexer", source = "hex", outer_checksum = "703D2347F5180B2BCEA4D258549B0D91DACD0905010892BAC46D04D913B84D1F" }, + { name = "simplifile", version = "0.1.14", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "10EA0207796F20488A3A166C50A189C9385333F3C9FAC187729DE7B9CE4ADDBC" }, + { name = "thoas", version = "0.4.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "4918D50026C073C4AB1388437132C77A6F6F7C8AC43C60C13758CC0ADCE2134E" }, +] + +[requirements] +exercism_test_runner = { version = "~> 1.4" } +gleam_bitwise = { version = "~> 1.2" } +gleam_otp = { version = "~> 0.7" } +gleam_stdlib = { version = "~> 0.30" } +simplifile = { version = "~> 0.1" } diff --git a/exercises/concept/expert-experiments/src/expert_experiments.gleam b/exercises/concept/expert-experiments/src/expert_experiments.gleam new file mode 100644 index 000000000..29fab578e --- /dev/null +++ b/exercises/concept/expert-experiments/src/expert_experiments.gleam @@ -0,0 +1,21 @@ +import gleam/result + +pub fn with_retry(experiment: fn() -> Result(t, e)) -> Result(t, e) { + todo +} + +pub fn record_timing( + time_logger: fn() -> Nil, + experiment: fn() -> Result(t, e), +) -> Result(t, e) { + todo +} + +pub fn run_experiment( + name: String, + setup: fn() -> Result(t, e), + action: fn(t) -> Result(u, e), + record: fn(t, u) -> Result(v, e), +) -> Result(#(String, v), e) { + todo +} diff --git a/exercises/concept/expert-experiments/test/expert_experiments_test.gleam b/exercises/concept/expert-experiments/test/expert_experiments_test.gleam new file mode 100644 index 000000000..68dece6fb --- /dev/null +++ b/exercises/concept/expert-experiments/test/expert_experiments_test.gleam @@ -0,0 +1,143 @@ +import expert_experiments +import gleam/erlang/process +import gleam/list +import exercism/test_runner +import exercism/should + +pub fn main() { + test_runner.main() +} + +fn mutable_yielder(elements: List(t)) -> fn() -> t { + let subject = process.new_subject() + list.each(elements, fn(element) { process.send(subject, element) }) + fn() { + case process.receive(subject, 0) { + Ok(element) -> element + Error(_) -> panic as "Callback called too many times" + } + } +} + +fn logger() -> #(fn(String) -> Nil, fn() -> List(String)) { + let subject = process.new_subject() + let writer = fn(message) { process.send(subject, message) } + let reader = fn() { read_all(subject, []) } + #(writer, reader) +} + +fn read_all( + subject: process.Subject(String), + read: List(String), +) -> List(String) { + case process.receive(subject, 0) { + Ok(message) -> read_all(subject, [message, ..read]) + Error(_) -> list.reverse(read) + } +} + +pub fn with_retry_pass_test() { + let function = mutable_yielder([Ok("First"), Error("Second")]) + { + use <- expert_experiments.with_retry + function() + } + |> should.equal(Ok("First")) +} + +pub fn with_retry_fail_fail_test() { + let function = mutable_yielder([Error("First"), Error("Second")]) + { + use <- expert_experiments.with_retry + function() + } + |> should.equal(Error("Second")) +} + +pub fn with_retry_fail_pass_test() { + let function = mutable_yielder([Error("First"), Ok("Second")]) + { + use <- expert_experiments.with_retry + function() + } + |> should.equal(Ok("Second")) +} + +pub fn record_timing_pass_test() { + let #(writer, reader) = logger() + { + use <- expert_experiments.record_timing(fn() { writer("timer") }) + writer("experiment") + Ok(0) + } + |> should.equal(Ok(0)) + + reader() + |> should.equal(["timer", "experiment", "timer"]) +} + +pub fn record_timing_fail_test() { + let #(writer, reader) = logger() + { + use <- expert_experiments.record_timing(fn() { writer("timer") }) + writer("experiment") + Error(Nil) + } + |> should.equal(Error(Nil)) + + reader() + |> should.equal(["timer", "experiment", "timer"]) +} + +pub fn run_experiment_fail_test() { + { + let setup = fn() { Error("Setup failed") } + let action = fn(_) { panic as "Should not run action" } + use _, _ <- expert_experiments.run_experiment("Experiment 1", setup, action) + panic as "Should not run record" + } + |> should.equal(Error("Setup failed")) +} + +pub fn run_experiment_pass_fail_test() { + { + let setup = fn() { Ok(1) } + let action = fn(x) { + should.equal(x, 1) + Error("Action failed") + } + use _, _ <- expert_experiments.run_experiment("Experiment 1", setup, action) + panic as "Should not run record" + } + |> should.equal(Error("Action failed")) +} + +pub fn run_experiment_pass_pass_fail_test() { + { + let setup = fn() { Ok(1) } + let action = fn(x) { + should.equal(x, 1) + Ok("2") + } + use x, y <- expert_experiments.run_experiment("Experiment 1", setup, action) + should.equal(x, 1) + should.equal(y, "2") + Error("Record failed") + } + |> should.equal(Error("Record failed")) +} + +pub fn run_experiment_pass_pass_pass_test() { + { + let setup = fn() { Ok(1) } + let action = fn(x) { + should.equal(x, 1) + Ok("2") + } + use x, y <- expert_experiments.run_experiment("Experiment 1", setup, action) + should.equal(x, 1) + should.equal(y, "2") + Ok("Success!") + } + |> should.equal(Ok(#("Experiment 1", "Success!"))) +}