Skip to content

Commit

Permalink
[gleam] complete sticker-shop
Browse files Browse the repository at this point in the history
  • Loading branch information
joaofnds committed May 17, 2024
1 parent a43130f commit 1803514
Show file tree
Hide file tree
Showing 8 changed files with 327 additions and 0 deletions.
4 changes: 4 additions & 0 deletions gleam/sticker-shop/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
*.beam
*.ez
build
erl_crash.dump
32 changes: 32 additions & 0 deletions gleam/sticker-shop/HELP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Help

## Running the tests

To run the tests, run the command `gleam test` from within the exercise directory.

## Submitting your solution

You can submit your solution using the `exercism submit src/sticker_shop.gleam` command.
This command will upload your solution to the Exercism website and print the solution page's URL.

It's possible to submit an incomplete solution which allows you to:

- See how others have completed the exercise
- Request help from a mentor

## Need to get help?

If you'd like help solving the exercise, check the following pages:

- The [Gleam track's documentation](https://exercism.org/docs/tracks/gleam)
- The [Gleam track's programming category on the forum](https://forum.exercism.org/c/programming/gleam)
- [Exercism's programming category on the forum](https://forum.exercism.org/c/programming/5)
- The [Frequently Asked Questions](https://exercism.org/docs/using/faqs)

Should those resources not suffice, you could submit your (incomplete) solution to request mentoring.

To get help if you're having trouble, you can use one of the following resources:

- [gleam.run](https://gleam.run/documentation/) is the gleam official documentation.
- [Discord](https://discord.gg/Fm8Pwmy) is the discord channel.
- [StackOverflow](https://stackoverflow.com/questions/tagged/gleam) can be used to search for your problem and see if it has been answered already. You can also ask and answer questions.
17 changes: 17 additions & 0 deletions gleam/sticker-shop/HINTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Hints

## 1. Define the `Usd`, `Eur`, and `Jpy` types

- The `Usd`, `Eur`, and `Jpy` types can be defined without constructors using the `pub type TypeName` syntax.

## 2. Define the `Money` type.

- The `Money` type should have a phantom type parameter for the currency.

## 3. Define `dollar`, `euro`, and `yen` functions

- The return annotation of the `dollar`, `euro`, and `yen` functions should be `Money(currency)` where `currency` is the appropriate type parameter of the `Money` type.

## 4. Define the `total` function

- The return annotation of the `total` function should use the same type parameter as the argument.
101 changes: 101 additions & 0 deletions gleam/sticker-shop/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Sticker Shop

Welcome to Sticker Shop on Exercism's Gleam Track.
If you need help running the tests or submitting your code, check out `HELP.md`.
If you get stuck on the exercise, check out `HINTS.md`, but try and solve it without using those first :)

## Introduction

## Phantom Types

Phantom types are type parameters of a custom type that are not used in any of the value constructors of that type.

That's a little abstract, so here is an example:

```gleam
pub type Length(unit) {
Length(amount: Float)
}
```

In this example the `unit` type parameter is not used in the `Length` value constructor, so `unit` is a phantom type.

This unused type parameter may seem useless, but it can be used to add further restrictions on how `Length` values can be used.

For example, we could have a function `double`, which multiplies the length. It works with lengths of any unit, so the type parameter is a generic type variable.

```gleam
// This function accepts all Length values
pub fn double(length: Length(unit)) -> Length(unit) {
Length(length.amount *. 2.0)
}
```

We could also have a function `add_inch`, which only works if the length is in inches.

```gleam
// A unit type for inches. It is never constructed so we don't
// define any constructors for it.
pub type Inches
pub fn add_inch(length: Length(Inches)) -> Length(Inches) {
Length(length.amount +. 1.0)
}
```

The `add_inch` function will not accept lengths of any other unit parameter, the phantom type has been used to ensure only the correct unit is used.

A function can also be written to ensure that two length values are of the same unit, by using the same type variable for both.

```gleam
pub fn add(a: Length(unit), b: Length(unit)) -> Length(unit) {
Length(a.amount +. b.amount)
}
```
```gleam
let two_meters: Length(Meters) = Length(2.0)
let two_inches: Length(Inches) = Length(2.0)
add(two_meters, two_meters)
// -> Length(4.0): Length(Meters)
add(two_meters, two_inches)
// Type error! The unit type parameters do not match.
```

Phantom types can work well with opaque types. If other modules cannot construct `Length` values then we can ensure they are not constructed with an invalid unit type, and that only the functions defined above can be used with them.

## Instructions

Lucy has an online sticker shop, where she sells cute stickers featuring everyone's favourite programming languages. People from all around the world buy her stickers, and she's having some trouble dealing with all the different currencies.

Create a program that Lucy can use to calculate prices while being sure that she's always using the correct currency.

## 1. Define the `Usd`, `Eur`, and `Jpy` types

These types are used to represent the different currencies that Lucy's customers use to buy her stickers.

They are to be used as phantom types and do not need to have any constructors.

## 2. Define the `Money` type.

The `Money` type should have an `Int` field for the amount of money, a currency phantom type parameter, and it should be an opaque type.

## 3. Define `dollar`, `euro`, and `yen` functions

Define the `dollar`, `euro`, and `yen` functions that take an `Int` argument and return a `Money` value with the correct currency.

## 4. Define the `total` function

Define the `total` function which takes a list of `Money` values and returns the total amount of money in the list.

```gleam
total([euro(120), euro(200), euro(145)])
// -> euro(465)
```

## Source

### Created by

- @lpil
12 changes: 12 additions & 0 deletions gleam/sticker-shop/gleam.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
name = "sticker_shop"
version = "0.1.0"

[dependencies]
gleam_bitwise = "~> 1.2"
gleam_otp = "~> 0.7 or ~> 1.0"
gleam_stdlib = "~> 0.32 or ~> 1.0"
simplifile = "~> 1.0"
gleam_erlang = ">= 0.25.0 and < 1.0.0"

[dev-dependencies]
exercism_test_runner = "~> 1.4"
27 changes: 27 additions & 0 deletions gleam/sticker-shop/manifest.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# This file was generated by Gleam
# You typically do not need to edit this file

packages = [
{ name = "argv", version = "1.0.1", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "A6E9009E50BBE863EB37D963E4315398D41A3D87D0075480FC244125808F964A" },
{ name = "exercism_test_runner", version = "1.7.0", build_tools = ["gleam"], requirements = ["argv", "gap", "glance", "gleam_community_ansi", "gleam_erlang", "gleam_json", "gleam_stdlib", "simplifile"], otp_app = "exercism_test_runner", source = "hex", outer_checksum = "2FC1BADB19BEC2AE77BFD2D3A606A014C85412A7B874CAFC4BA8CF04B0B257CD" },
{ name = "gap", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_stdlib"], otp_app = "gap", source = "hex", outer_checksum = "2EE1B0A17E85CF73A0C1D29DA315A2699117A8F549C8E8D89FA8261BE41EDEB1" },
{ name = "glance", version = "0.8.2", build_tools = ["gleam"], requirements = ["gleam_stdlib", "glexer"], otp_app = "glance", source = "hex", outer_checksum = "ACF09457E8B564AD7A0D823DAFDD326F58263C01ACB0D432A9BEFDEDD1DA8E73" },
{ name = "gleam_bitwise", version = "1.3.1", build_tools = ["gleam"], requirements = [], otp_app = "gleam_bitwise", source = "hex", outer_checksum = "B36E1D3188D7F594C7FD4F43D0D2CE17561DE896202017548578B16FE1FE9EFC" },
{ name = "gleam_community_ansi", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "FE79E08BF97009729259B6357EC058315B6FBB916FAD1C2FF9355115FEB0D3A4" },
{ name = "gleam_community_colour", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "A49A5E3AE8B637A5ACBA80ECB9B1AFE89FD3D5351FF6410A42B84F666D40D7D5" },
{ name = "gleam_erlang", version = "0.25.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "054D571A7092D2A9727B3E5D183B7507DAB0DA41556EC9133606F09C15497373" },
{ name = "gleam_json", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "8B197DD5D578EA6AC2C0D4BDC634C71A5BCA8E7DB5F47091C263ECB411A60DF3" },
{ name = "gleam_otp", version = "0.10.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "0B04FE915ACECE539B317F9652CAADBBC0F000184D586AAAF2D94C100945D72B" },
{ name = "gleam_stdlib", version = "0.36.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "C0D14D807FEC6F8A08A7C9EF8DFDE6AE5C10E40E21325B2B29365965D82EB3D4" },
{ name = "glexer", version = "0.7.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glexer", source = "hex", outer_checksum = "4484942A465482A0A100936E1E5F12314DB4B5AC0D87575A7B9E9062090B96BE" },
{ name = "simplifile", version = "1.5.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "EB9AA8E65E5C1E3E0FDCFC81BC363FD433CB122D7D062750FFDF24DE4AC40116" },
{ 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_erlang = { version = ">= 0.25.0 and < 1.0.0"}
gleam_otp = { version = "~> 0.7 or ~> 1.0" }
gleam_stdlib = { version = "~> 0.32 or ~> 1.0" }
simplifile = { version = "~> 1.0" }
29 changes: 29 additions & 0 deletions gleam/sticker-shop/src/sticker_shop.gleam
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import gleam/list

pub type Usd

pub type Eur

pub type Jpy

pub opaque type Money(currency) {
Money(amount: Int)
}

pub fn dollar(amount: Int) -> Money(Usd) {
Money(amount)
}

pub fn euro(amount: Int) -> Money(Eur) {
Money(amount)
}

pub fn yen(amount: Int) -> Money(Jpy) {
Money(amount)
}

pub fn total(prices: List(Money(currency))) -> Money(currency) {
prices
|> list.fold(0, fn(total, price) { total + price.amount })
|> Money
}
105 changes: 105 additions & 0 deletions gleam/sticker-shop/test/sticker_shop_test.gleam
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import exercism/should
import exercism/test_runner
import gleam/string
import simplifile
import sticker_shop.{type Eur, type Jpy, type Money, type Usd}

pub fn main() {
test_runner.main()
}

fn compact_whitespace(graphemes: List(String), acc: String) -> String {
case graphemes {
[] -> acc
[" ", " ", ..rest] -> compact_whitespace([" ", ..rest], acc)
[grapheme, ..rest] -> compact_whitespace(rest, acc <> grapheme)
}
}

fn read_source() -> String {
let assert Ok(src) = simplifile.read("src/sticker_shop.gleam")
src
|> string.replace("\n", "")
|> string.to_graphemes
|> compact_whitespace("")
|> string.replace(" (", "(")
|> string.replace(")->", ") ->")
}

pub fn type_must_be_opaque_test() {
let src = read_source()
case string.contains(src, "pub opaque type Money") {
True -> Nil
False -> panic as "The Money type must exist and be opaque"
}
}

pub fn dollar_function_must_return_usd_test() {
let src = read_source()
case string.contains(src, "-> Money(Usd)") {
True -> Nil
False -> panic as "The dollar function must return Usd"
}
}

pub fn euro_function_must_return_eur_test() {
let src = read_source()
case string.contains(src, "-> Money(Eur)") {
True -> Nil
False -> panic as "The euro function must return Eur"
}
}

pub fn yen_function_must_return_jpy_test() {
let src = read_source()
case string.contains(src, "-> Money(Jpy)") {
True -> Nil
False -> panic as "The yen function must return Jpy"
}
}

pub fn dollar_test() {
let _money: Money(Usd) = sticker_shop.dollar(1)
}

pub fn euro_test() {
let _money: Money(Eur) = sticker_shop.euro(1)
}

pub fn yen_test() {
let _money: Money(Jpy) = sticker_shop.yen(1)
}

pub fn total_dollars_test() {
[
sticker_shop.dollar(120),
sticker_shop.dollar(50),
sticker_shop.dollar(45),
sticker_shop.dollar(100),
]
|> sticker_shop.total
|> should.equal(sticker_shop.dollar(315))
}

pub fn total_euros_test() {
[
sticker_shop.euro(110),
sticker_shop.euro(20),
sticker_shop.euro(35),
sticker_shop.euro(100),
]
|> sticker_shop.total
|> should.equal(sticker_shop.euro(265))
}

pub fn total_yen_test() {
[
sticker_shop.yen(480),
sticker_shop.yen(340),
sticker_shop.yen(455),
sticker_shop.yen(165),
sticker_shop.yen(100),
]
|> sticker_shop.total
|> should.equal(sticker_shop.yen(1540))
}

0 comments on commit 1803514

Please sign in to comment.