tame/tamer/src/f.rs

175 lines
6.5 KiB
Rust
Raw Normal View History

tamer: f::Functor: New trait This commit is purposefully coupled with changes that utilize it to demonstrate that the need for this abstraction has been _derived_, not forced; TAMER doesn't aim to be functional for the sake of it, since idiomatic Rust achieves many of its benefits without the formalisms. But, the formalisms do occasionally help, and this is one such example. There is other existing code that can be refactored to take advantage of this style as well. I do _not_ wish to pull an existing functional dependency into TAMER; I want to keep these abstractions light, and eliminate them as necessary, as Rust continues to integrate new features into its core. I also want to be able to modify the abstractions to suit our particular needs. (This is _not_ a general recommendation; it's particular to TAMER and to my experience.) This implementation of `Functor` is one such example. While it is modeled after Haskell in that it provides `fmap`, the primitive here is instead `map`, with `fmap` derived from it, since `map` allows for better use of Rust idioms. Furthermore, it's polymorphic over _trait_ type parameters, not method, allowing for separate trait impls for different container types, which can in turn be inferred by Rust and allow for some very concise mapping; this is particularly important for TAMER because of the disciplined use of newtypes. For example, `foo.overwrite(span)` and `foo.overwrite(name)` are both self-documenting, and better alternatives than, say, `foo.map_span(|_| span)` and `foo.map_symbol(|_| name)`; the latter are perfectly clear in what they do, but lack a layer of abstraction, and are verbose. But the clarity of the _new_ form does rely on either good naming conventions of arguments, or explicit type annotations using turbofish notation if necessary. This will be implemented on core Rust types as appropriate and as possible. At the time of writing, we do not yet have trait specialization, and there's too many soundness issues for me to be comfortable enabling it, so that limits that we can do with something like, say, a generic `Result`, while also allowing for specialized implementations based on newtypes. DEV-13160
2023-01-04 12:30:18 -05:00
// Functional primitives and conveniences
//
// Copyright (C) 2014-2023 Ryan Specialty, LLC.
tamer: f::Functor: New trait This commit is purposefully coupled with changes that utilize it to demonstrate that the need for this abstraction has been _derived_, not forced; TAMER doesn't aim to be functional for the sake of it, since idiomatic Rust achieves many of its benefits without the formalisms. But, the formalisms do occasionally help, and this is one such example. There is other existing code that can be refactored to take advantage of this style as well. I do _not_ wish to pull an existing functional dependency into TAMER; I want to keep these abstractions light, and eliminate them as necessary, as Rust continues to integrate new features into its core. I also want to be able to modify the abstractions to suit our particular needs. (This is _not_ a general recommendation; it's particular to TAMER and to my experience.) This implementation of `Functor` is one such example. While it is modeled after Haskell in that it provides `fmap`, the primitive here is instead `map`, with `fmap` derived from it, since `map` allows for better use of Rust idioms. Furthermore, it's polymorphic over _trait_ type parameters, not method, allowing for separate trait impls for different container types, which can in turn be inferred by Rust and allow for some very concise mapping; this is particularly important for TAMER because of the disciplined use of newtypes. For example, `foo.overwrite(span)` and `foo.overwrite(name)` are both self-documenting, and better alternatives than, say, `foo.map_span(|_| span)` and `foo.map_symbol(|_| name)`; the latter are perfectly clear in what they do, but lack a layer of abstraction, and are verbose. But the clarity of the _new_ form does rely on either good naming conventions of arguments, or explicit type annotations using turbofish notation if necessary. This will be implemented on core Rust types as appropriate and as possible. At the time of writing, we do not yet have trait specialization, and there's too many soundness issues for me to be comfortable enabling it, so that limits that we can do with something like, say, a generic `Result`, while also allowing for specialized implementations based on newtypes. DEV-13160
2023-01-04 12:30:18 -05:00
//
// This file is part of TAME.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! Functional primitives and conveniences.
//!
//! TODO: More information on TAMER's stance on Rust,
//! the architecture of the compiler,
//! and how that squares with functional programming.
//!
//! The definitions in this module are _derived from needs in TAMER_.
//! This is _not_ intended to be a comprehensive functional programming
//! library,
//! nor is such a thing desirable.
//!
//! This module is named `f` rather than `fn` because the latter is a
//! reserved word in Rust and makes for awkward (`r#`-prefixed) imports.
/// A type providing a `map` function from inner type `T` to `U`.
///
/// In an abuse of terminology,
/// this functor is polymorphic over the entire trait,
/// rather than just the definition of `map`,
/// allowing implementations to provide multiple specialized `map`s
/// without having to define individual `map_*` methods.
/// Rust will often be able to infer the proper types and invoke the
/// intended `map` function within a given context,
/// but methods may still be explicitly disambiguated using the
/// turbofish notation if this is too confusing in context.
/// Strictly speaking,
/// if a functor requires a monomorphic function
/// (so `T = U`),
/// then it's not really a functor.
/// We'll refer to these structures informally as monomorphic functors,
/// since they provide the same type of API as a functor,
/// but cannot change the underlying type.
///
/// This trait also provides a number of convenience methods that can be
/// implemented in terms of [`Functor::map`].
///
/// Why a primitive `map` instead of `fmap`?
/// ========================================
/// One of the methods of this trait is [`Functor::fmap`],
/// which [is motivated by Haskell][haskell-functor].
/// This trait implements methods in terms of [`map`](Self::map) rather than
/// [`fmap`](Self::fmap) because `map` is a familiar idiom in Rust and
/// fits most naturally with surrounding idiomatic Rust code;
/// there is no loss in generality in doing so.
/// Furthermore,
/// implementing `fmap` requires the use of moved closures,
/// which is additional boilerplate relative to `map`.
///
/// [haskell-functor]: https://hackage.haskell.org/package/base/docs/Data-Functor.html
pub trait Functor<T, U = T>: Sized {
/// Type of object resulting from [`Functor::map`] operation.
///
/// The term "target" originates from category theory,
/// representing the codomain of the functor.
type Target = Self;
/// A structure-preserving map between types `T` and `U`.
///
/// This unwraps any number of `T` from `Self` and applies the
/// function `f` to transform it into `U`,
/// wrapping the result back up into [`Self`].
///
/// This is the only method that needs to be implemented on this trait;
/// all others are implemented in terms of it.
fn map(self, f: impl FnOnce(T) -> U) -> Self::Target;
/// Curried form of [`Functor::map`],
/// with arguments reversed.
///
/// `fmap` returns a unary closure that accepts an object of type
/// [`Self`].
/// This is more amenable to function composition and a point-free style.
fn fmap(f: impl FnOnce(T) -> U) -> impl FnOnce(Self) -> Self::Target {
move |x| x.map(f)
}
/// Map over self,
/// replacing each mapped element with `value`.
///
/// This is equivalent to mapping with an identity function.
///
/// The name `overwrite` was chosen because `replace` is already used in
/// core Rust libraries to overwrite and then _return_ the original
/// value,
/// whereas this function overwrites and then returns
/// [`Self::Target`].
///
/// This is intended for cases where there's a single element that will
/// be replaced,
/// taking advantage of [`Functor`]'s trait-level polymorphism.
fn overwrite(self, value: U) -> Self::Target {
self.map(|_| value)
}
/// Curried form of [`Functor::overwrite`],
/// with arguments reversed.
fn foverwrite(value: U) -> impl FnOnce(Self) -> Self::Target {
move |x| x.overwrite(value)
}
}
impl<T, U> Functor<T, U> for Option<T> {
type Target = Option<U>;
fn map(self, f: impl FnOnce(T) -> U) -> <Self as Functor<T, U>>::Target {
Option::map(self, f)
}
}
/// A nullary [`Fn`] delaying some computation.
///
/// For the history and usage of this term in computing,
/// see <https://en.wikipedia.org/wiki/Thunk>.
pub trait Thunk<T> = Fn() -> T;
/// Data represented either as a reference with a `'static` lifetime
/// (representing a computation already having been performed),
/// or a [`Thunk`] that will produce similar data when invoked.
///
/// The purpose of this trait is to force an API to defer potentially
/// expensive computations in situations where it may be too easy to
/// accidentally do too much work due to Rust's eager argument evaluation
/// strategy
/// (e.g. see Clippy's `expect_fun_call` lint).
///
/// This is sort of like a static [`std::borrow::Cow`],
/// in the sense that it can hold a reference or owned value;
/// a thunk can return a value of any type or lifetime
/// (including owned),
/// whereas non-thunks require static references.
/// _The burden is on the trait implementation to enforce these
/// constraints._
pub trait ThunkOrStaticRef<T: ?Sized> {
type Output: AsRef<T>;
/// Force the [`Thunk`] or return the static reference,
/// depending on the type of [`Self`].
fn call(self) -> Self::Output;
}
impl<T: ?Sized, R: AsRef<T>, F: Fn() -> R> ThunkOrStaticRef<T> for F {
type Output = R;
fn call(self) -> R {
self()
}
}
impl ThunkOrStaticRef<str> for &'static str {
type Output = &'static str;
fn call(self) -> &'static str {
self
}
}