From 56944d94e84696f478c9ebaeda05e1927324e2b6 Mon Sep 17 00:00:00 2001 From: Sean Haugh Date: Sat, 20 Apr 2024 10:33:55 -0500 Subject: [PATCH] feat: add equivalence and predicate modules --- .github/workflows/{test.yml => tests.yml} | 2 +- README.md | 70 ++++++- gleam.toml | 15 +- manifest.toml | 11 + src/ask.gleam | 5 - src/ask/equivalence.gleam | 69 +++++++ src/ask/predicate.gleam | 82 ++++++++ test/ask_test.gleam | 236 +++++++++++++++++++++- 8 files changed, 465 insertions(+), 25 deletions(-) rename .github/workflows/{test.yml => tests.yml} (98%) create mode 100644 manifest.toml delete mode 100644 src/ask.gleam create mode 100644 src/ask/equivalence.gleam create mode 100644 src/ask/predicate.gleam diff --git a/.github/workflows/test.yml b/.github/workflows/tests.yml similarity index 98% rename from .github/workflows/test.yml rename to .github/workflows/tests.yml index 12b9772..abd4d01 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/tests.yml @@ -8,7 +8,7 @@ on: pull_request: jobs: - test: + tests: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/README.md b/README.md index dd1eaef..27c0312 100644 --- a/README.md +++ b/README.md @@ -2,18 +2,80 @@ [![Package Version](https://img.shields.io/hexpm/v/ask)](https://hex.pm/packages/ask) [![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/ask/) +![CI](https://github.com/furrycatherder/gleam-ask/workflows/tests/badge.svg?branch=main) + +`ask` is a Gleam library tailored for simplifying the management of equivalences +and predicates. Equivalences allow for the comparison of two values to +determine if they're equivalent, while predicates assess if a given value meets +specific conditions. + +## Overview + +`ask` provides developers with a set of functions to create, combine, and +transform equivalences and predicates. Whether you're performing basic value +comparisons or intricate logical operations, `ask` furnishes the necessary tools +to handle a variety of scenarios effectively. + +## Installation ```sh gleam add ask ``` + +## Usage + +### Equivalences + ```gleam -import ask +import ask/equivalence + +// Defining custom equivalences +let eq1 = fn(x, y) -> Bool { x == y } +let eq2 = fn(x, y) -> Bool { x % 2 == y % 2 } + +// Combining equivalences using logical operations +let combined_eq = equivalence.and(eq1, eq2) -pub fn main() { - // TODO: An example of the project in use -} +// Using equivalence to compare values +let result = combined_eq(4, 8) // Returns True ``` +Equivalences are expected to follow these friendly rules: + +- **Reflexivity**: Every value is equivalent to itself. It's like saying, "Hey, + you're always equal to yourself!" +- **Symmetry**: If one value is equivalent to another, then it's a two-way + street! If X is like Y, then Y is like X. It's all about fairness! +- **Transitivity**: Imagine a chain of equivalence! If X is equivalent to Y, + and Y is equivalent to Z, then it's like saying X is also buddies with Z. + Friendship circles all around! + +These rules help keep our equivalences reliable and predictable, making sure +they play nice with each other. + +### Predicates + +```gleam +import ask/predicate + +// Defining custom predicates +let is_positive = fn(x) -> Bool { x > 0 } +let is_even = fn(x) -> Bool { x % 2 == 0 } + +// Combining predicates using logical operations +let combined_pred = predicate.and(is_positive, is_even) + +// Using predicate to evaluate values +let result = combined_pred(6) // Returns True +``` + +## Conclusion + +`ask` equips Gleam developers with a robust toolkit for managing equivalences and +predicates efficiently. Whether you're implementing algorithms, data validation +systems, or decision-making processes, `ask` facilitates streamlined code +development, allowing you to focus on problem-solving. + Further documentation can be found at . ## Development diff --git a/gleam.toml b/gleam.toml index 9b6b60c..a87ce3f 100644 --- a/gleam.toml +++ b/gleam.toml @@ -1,16 +1,9 @@ name = "ask" -version = "1.0.0" +version = "0.1.0" -# Fill out these fields if you intend to generate HTML documentation or publish -# your project to the Hex package manager. -# -# description = "" -# licences = ["Apache-2.0"] -# repository = { type = "github", user = "username", repo = "project" } -# links = [{ title = "Website", href = "https://gleam.run" }] -# -# For a full reference of all the available options, you can have a look at -# https://gleam.run/writing-gleam/gleam-toml/. +description = "Utilities for composing predicates and equivalence relations" +licences = ["Apache-2.0"] +repository = { type = "github", user = "furrycatherder", repo = "gleam-ask" } [dependencies] gleam_stdlib = ">= 0.34.0 and < 2.0.0" diff --git a/manifest.toml b/manifest.toml new file mode 100644 index 0000000..085a43c --- /dev/null +++ b/manifest.toml @@ -0,0 +1,11 @@ +# This file was generated by Gleam +# You typically do not need to edit this file + +packages = [ + { name = "gleam_stdlib", version = "0.37.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "5398BD6C2ABA17338F676F42F404B9B7BABE1C8DC7380031ACB05BBE1BCF3742" }, + { name = "gleeunit", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "72CDC3D3F719478F26C4E2C5FED3E657AC81EC14A47D2D2DEBB8693CA3220C3B" }, +] + +[requirements] +gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" } +gleeunit = { version = ">= 1.0.0 and < 2.0.0" } diff --git a/src/ask.gleam b/src/ask.gleam deleted file mode 100644 index f6a3214..0000000 --- a/src/ask.gleam +++ /dev/null @@ -1,5 +0,0 @@ -import gleam/io - -pub fn main() { - io.println("Hello from ask!") -} diff --git a/src/ask/equivalence.gleam b/src/ask/equivalence.gleam new file mode 100644 index 0000000..380b751 --- /dev/null +++ b/src/ask/equivalence.gleam @@ -0,0 +1,69 @@ +pub type Equivalence(a) = + fn(a, a) -> Bool + +/// Create a new equivalence that always returns `True`. +/// +/// This is sometimes called the trivial or universal equivalence. +/// +pub fn trivial() -> Equivalence(a) { + fn(_, _) -> Bool { True } +} + +/// Combine two equivalences into a new equivalence that returns `True` if both +/// equivalences return `True`. +/// +pub fn and(first: Equivalence(a), second: Equivalence(a)) -> Equivalence(a) { + fn(value: a, other: a) -> Bool { first(value, other) && second(value, other) } +} + +/// Combine two equivalences into a new equivalence that returns `True` if either +/// equivalence returns `True`. +/// +pub fn or(first: Equivalence(a), second: Equivalence(a)) -> Equivalence(a) { + fn(value: a, other: a) -> Bool { first(value, other) || second(value, other) } +} + +/// Negate an equivalence to create a new equivalence that returns `True` if the +/// original equivalence returns `False`. +/// +pub fn not(eq: Equivalence(a)) -> Equivalence(a) { + fn(value: a, other: a) -> Bool { !eq(value, other) } +} + +/// Create a new equivalence for a list of values based on the given equivalence. +/// +pub fn list(eq: Equivalence(a)) -> Equivalence(List(a)) { + fn(values: List(a), others: List(a)) -> Bool { + case values, others { + [], [] -> True + [value, ..values], [other, ..others] -> { + eq(value, other) && list(eq)(values, others) + } + _, _ -> False + } + } +} + +/// Create a new equivalence for a pair of values based on the given equivalences. +/// +pub fn pair( + first: Equivalence(a), + second: Equivalence(b), +) -> Equivalence(#(a, b)) { + fn(value: #(a, b), other: #(a, b)) -> Bool { + case value, other { + #(value1, value2), #(other1, other2) -> { + first(value1, other1) && second(value2, other2) + } + } + } +} + +/// Map the input of an equivalence to create a new equivalence. +/// +pub fn map_input( + over eq: Equivalence(a), + with fun: fn(b) -> a, +) -> Equivalence(b) { + fn(value: b, other: b) -> Bool { eq(fun(value), fun(other)) } +} diff --git a/src/ask/predicate.gleam b/src/ask/predicate.gleam new file mode 100644 index 0000000..ca8544d --- /dev/null +++ b/src/ask/predicate.gleam @@ -0,0 +1,82 @@ +import gleam/list + +pub type Predicate(a) = + fn(a) -> Bool + +/// Create a new predicate that always returns `True`. +/// +pub fn always() -> Predicate(a) { + fn(_) -> Bool { True } +} + +/// Create a new predicate that always returns `False`. +/// +pub fn never() -> Predicate(a) { + fn(_) -> Bool { False } +} + +/// Combine two predicates together into a new predicate that returns `True` if +/// both predicates return `True`. +/// +pub fn and(first: Predicate(a), second: Predicate(a)) -> Predicate(a) { + fn(value: a) -> Bool { first(value) && second(value) } +} + +/// Combine two predicates together into a new predicate that returns `True` if +/// either predicate returns `True`. +/// +pub fn or(first: Predicate(a), second: Predicate(a)) -> Predicate(a) { + fn(value: a) -> Bool { first(value) || second(value) } +} + +/// Negate a predicate. +/// +pub fn not(p: Predicate(a)) -> Predicate(a) { + fn(value: a) -> Bool { !p(value) } +} + +/// Combine a list of predicates together into a new predicate that returns `True` +/// if all predicates return `True`. +/// +pub fn every(ps: List(Predicate(a))) -> Predicate(a) { + case ps { + [] -> fn(_) -> Bool { True } + [p, ..ps] -> fn(value: a) -> Bool { p(value) && every(ps)(value) } + } +} + +/// Combine a list of predicates together into a new predicate that returns `True` +/// if any predicate returns `True`. +/// +pub fn some(ps: List(Predicate(a))) -> Predicate(a) { + case ps { + [] -> fn(_) -> Bool { False } + [p, ..ps] -> fn(value: a) -> Bool { p(value) || some(ps)(value) } + } +} + +/// Create a new predicate that returns `True` if it returns `True` for every +/// element in a list. +/// +pub fn all(p: Predicate(a)) -> Predicate(List(a)) { + fn(a: List(a)) -> Bool { + a + |> list.all(p) + } +} + +/// Create a new predicate that returns `True` if it returns `True` for any +/// element in a list. +/// +pub fn any(p: Predicate(a)) -> Predicate(List(a)) { + fn(a: List(a)) -> Bool { + a + |> list.any(p) + } +} + +/// Map the input of a predicate to create a new predicate. +/// +pub fn map_input(over p: Predicate(a), with fun: fn(b) -> a) -> Predicate(b) { + fn(b: b) -> Bool { p(fun(b)) } +} diff --git a/test/ask_test.gleam b/test/ask_test.gleam index 3831e7a..0d1f0cb 100644 --- a/test/ask_test.gleam +++ b/test/ask_test.gleam @@ -1,3 +1,7 @@ +import ask/equivalence +import ask/predicate.{always, never} +import gleam/int +import gleam/string import gleeunit import gleeunit/should @@ -5,8 +9,232 @@ pub fn main() { gleeunit.main() } -// gleeunit test functions end in `_test` -pub fn hello_world_test() { - 1 - |> should.equal(1) +/// Put whatever here... +const a = 0 + +pub fn predicate_and_test() { + predicate.and(never(), never())(a) + |> should.equal(False) + + predicate.and(never(), always())(a) + |> should.equal(False) + + predicate.and(always(), never())(a) + |> should.equal(False) + + predicate.and(always(), always())(a) + |> should.equal(True) +} + +pub fn predicate_or_test() { + predicate.or(never(), never())(a) + |> should.equal(False) + + predicate.or(never(), always())(a) + |> should.equal(True) + + predicate.or(always(), never())(a) + |> should.equal(True) + + predicate.or(always(), always())(a) + |> should.equal(True) +} + +pub fn predicate_not_test() { + predicate.not(always())(a) + |> should.equal(False) + + predicate.not(never())(a) + |> should.equal(True) +} + +pub fn predicate_every_test() { + predicate.every([])(a) + |> should.equal(True) + + predicate.every([always()])(a) + |> should.equal(True) + + predicate.every([never()])(a) + |> should.equal(False) + + predicate.every([always(), always()])(a) + |> should.equal(True) + + predicate.every([always(), never()])(a) + |> should.equal(False) + + predicate.every([never(), always()])(a) + |> should.equal(False) + + predicate.every([never(), never()])(a) + |> should.equal(False) +} + +pub fn predicate_some_test() { + predicate.some([])(a) + |> should.equal(False) + + predicate.some([always()])(a) + |> should.equal(True) + + predicate.some([never()])(a) + |> should.equal(False) + + predicate.some([always(), always()])(a) + |> should.equal(True) + + predicate.some([always(), never()])(a) + |> should.equal(True) + + predicate.some([never(), always()])(a) + |> should.equal(True) + + predicate.some([never(), never()])(a) + |> should.equal(False) +} + +pub fn predicate_all_test() { + predicate.all(never())([]) + |> should.equal(True) + + predicate.all(never())([a]) + |> should.equal(False) + + predicate.all(always())([]) + |> should.equal(True) + + predicate.all(always())([a]) + |> should.equal(True) +} + +pub fn predicate_any_test() { + predicate.any(never())([]) + |> should.equal(False) + + predicate.any(never())([a]) + |> should.equal(False) + + predicate.any(always())([]) + |> should.equal(False) + + predicate.any(always())([a]) + |> should.equal(True) +} + +pub fn predicate_map_input_test() { + predicate.map_input(always(), fn(_) { False })(a) + |> should.equal(True) + + predicate.map_input(always(), fn(_) { True })(a) + |> should.equal(True) + + predicate.map_input(never(), fn(_) { False })(a) + |> should.equal(False) + + predicate.map_input(never(), fn(_) { True })(a) + |> should.equal(False) + + predicate.map_input(int.is_even, string.length)("hello") + |> should.equal(False) + + predicate.map_input(int.is_even, string.length)("world!") + |> should.equal(True) +} + +pub fn eq_trivial_test() { + equivalence.trivial()(1, 1) + |> should.equal(True) + + equivalence.trivial()(1, 2) + |> should.equal(True) +} + +pub fn eq_and_test() { + let eq1 = fn(a, b) { a == b } + let eq2 = fn(a, b) { a % 2 == b % 2 } + let and_eq = equivalence.and(eq1, eq2) + + and_eq(2, 2) + |> should.equal(True) + + and_eq(3, 3) + |> should.equal(True) + + and_eq(2, 3) + |> should.equal(False) +} + +pub fn eq_or_test() { + let eq1 = fn(a, b) { a == b } + let eq2 = fn(a, b) { a % 2 == b % 2 } + let or_eq = equivalence.or(eq1, eq2) + + or_eq(2, 2) + |> should.equal(True) + + or_eq(3, 3) + |> should.equal(True) + + or_eq(2, 4) + |> should.equal(True) + + or_eq(3, 6) + |> should.equal(False) + + or_eq(1, 2) + |> should.equal(False) +} + +pub fn eq_not_test() { + let eq = fn(a, b) { a == b } + let not_eq = equivalence.not(eq) + + not_eq(1, 1) + |> should.equal(False) + + not_eq(1, 2) + |> should.equal(True) +} + +pub fn eq_list_test() { + let eq = fn(a, b) { a == b } + let all_eq = equivalence.list(eq) + + all_eq([1, 1, 1], [1, 1, 1]) + |> should.equal(True) + + all_eq([1, 1, 1], [1, 1, 2]) + |> should.equal(False) + + all_eq([1, 1, 1], [1, 1]) + |> should.equal(False) +} + +pub fn eq_pair_test() { + let eq = fn(a, b) { a == b } + let pair_eq = equivalence.pair(eq, eq) + + pair_eq(#(1, 1), #(1, 1)) + |> should.equal(True) + + pair_eq(#(1, 1), #(1, 2)) + |> should.equal(False) +} + +pub fn eq_map_input_test() { + let eq = fn(a, b) { a == b } + let map_eq = equivalence.map_input(eq, fn(x) { x % 2 }) + + map_eq(1, 1) + |> should.equal(True) + + map_eq(2, 4) + |> should.equal(True) + + map_eq(3, 5) + |> should.equal(True) + + map_eq(1, 2) + |> should.equal(False) }