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
main
Mike Gerwitz 2023-01-04 12:30:18 -05:00
parent 0784dc306e
commit edbfc87a54
10 changed files with 163 additions and 15 deletions

View File

@ -22,6 +22,7 @@ use super::{
};
use crate::{
asg::Expr,
f::Functor,
fmt::{DisplayWrapper, TtQuote},
parse::{self, util::SPair, ParseState, Token, Transition, Transitionable},
span::{Span, UNKNOWN_SPAN},
@ -260,7 +261,7 @@ impl ParseState for AirAggregate {
(ReachableExpr(oi), CloseExpr(end)) => {
let _ = asg.mut_map_obj::<Expr>(oi, |expr| {
expr.map_span(|span| span.merge(end).unwrap_or(span))
expr.map(|span| span.merge(end).unwrap_or(span))
});
Transition(Empty).incomplete()

View File

@ -21,7 +21,7 @@
use std::fmt::Display;
use crate::{num::Dim, span::Span};
use crate::{f::Functor, num::Dim, span::Span};
/// Expression.
///
@ -41,8 +41,10 @@ impl Expr {
Expr(_, _, span) => *span,
}
}
}
pub fn map_span(self, f: impl FnOnce(Span) -> Span) -> Self {
impl Functor<Span> for Expr {
fn map(self, f: impl FnOnce(Span) -> Span) -> Self {
match self {
Self(op, dim, span) => Self(op, dim, f(span)),
}

View File

@ -25,6 +25,7 @@ use super::{
};
use crate::diagnose::panic::DiagnosticPanic;
use crate::diagnose::Annotate;
use crate::f::Functor;
use crate::global;
use crate::parse::util::SPair;
use crate::parse::Token;
@ -483,7 +484,7 @@ impl Asg {
// as referenced in the above panic.
obj_container.replace(new_obj);
index.map_span(|_| new_span)
index.overwrite(new_span)
}
/// Map over an inner object that is referenced by an identifier.

View File

@ -23,6 +23,7 @@ use std::fmt::Display;
use crate::{
diagnose::{Annotate, Diagnostic},
f::Functor,
fmt::{DisplayWrapper, TtQuote},
num::{Dim, Dtype},
parse::{util::SPair, Token},
@ -250,7 +251,7 @@ impl Ident {
// Note that this has the effect of clearing fragments if we
// originally were in state `Ident::IdentFragment`.
Ok(Ident::Ident(name.map_span(|_| span), kind, src))
Ok(Ident::Ident(name.overwrite(span), kind, src))
}
// If we encountered the override _first_,
@ -285,7 +286,7 @@ impl Ident {
return Err((self, err));
}
Ok(Ident::Ident(name.map_span(|_| span), kind, src))
Ok(Ident::Ident(name.overwrite(span), kind, src))
}
// These represent the prologue and epilogue of maps.
@ -299,7 +300,7 @@ impl Ident {
) => Ok(self),
Ident::Missing(name) => {
Ok(Ident::Ident(name.map_span(|_| span), kind, src))
Ok(Ident::Ident(name.overwrite(span), kind, src))
}
// TODO: Remove guards and catch-all for exhaustiveness check.
@ -360,9 +361,7 @@ impl Ident {
src: Source,
) -> TransitionResult<Ident> {
match self.kind() {
None => {
Ok(Ident::Extern(self.name().map_span(|_| span), kind, src))
}
None => Ok(Ident::Extern(self.name().overwrite(span), kind, src)),
Some(cur_kind) => {
if cur_kind != &kind {
let err = TransitionError::ExternResolution(

View File

@ -65,6 +65,7 @@ use super::{Expr, Ident};
use crate::{
diagnose::Annotate,
diagnostic_panic,
f::Functor,
span::{Span, UNKNOWN_SPAN},
};
use petgraph::graph::NodeIndex;
@ -274,8 +275,10 @@ impl<O: ObjectKind> ObjectIndex<O> {
pub fn new(index: NodeIndex, span: Span) -> Self {
Self(index, span, PhantomData::default())
}
}
pub fn map_span(self, f: impl FnOnce(Span) -> Span) -> Self {
impl<O: ObjectKind> Functor<Span> for ObjectIndex<O> {
fn map(self, f: impl FnOnce(Span) -> Span) -> Self {
match self {
Self(index, span, ph) => Self(index, f(span), ph),
}

127
tamer/src/f.rs 100644
View File

@ -0,0 +1,127 @@
// Functional primitives and conveniences
//
// Copyright (C) 2014-2022 Ryan Specialty Group, LLC.
//
// 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)
}
}

View File

@ -70,6 +70,12 @@
// this is largely experimentation to see if it's useful.
#![allow(incomplete_features)]
#![feature(adt_const_params)]
// Used for traits returning functions,
// such as those in `crate::f`.
// Our use of this feature is fairly basic;
// should it become too complex then we should re-evaluate what we ought
// to be doing relative to the status of this feature.
#![feature(return_position_impl_trait_in_trait)]
// We build docs for private items.
#![allow(rustdoc::private_intra_doc_links)]
// For sym::prefill recursive macro `static_symbols!`.
@ -86,6 +92,7 @@ pub mod xir;
pub mod asg;
pub mod convert;
pub mod diagnose;
pub mod f;
pub mod fmt;
pub mod fs;
pub mod iter;

View File

@ -55,6 +55,7 @@ mod parse;
use crate::{
diagnose::{Annotate, Diagnostic},
f::Functor,
fmt::{DisplayWrapper, TtQuote},
parse::{util::SPair, Object, Token},
span::{Span, UNKNOWN_SPAN},
@ -139,7 +140,9 @@ impl Nir {
}
}
}
}
impl Functor<SymbolId> for Nir {
/// Map over a token's [`SymbolId`].
///
/// This allows modifying a token's [`SymbolId`] while retaining the
@ -154,7 +157,7 @@ impl Nir {
///
/// See also [`Nir::symbol`] if you only wish to retrieve the symbol
/// rather than map over it.
pub fn map(self, f: impl FnOnce(SymbolId) -> SymbolId) -> Self {
fn map(self, f: impl FnOnce(SymbolId) -> SymbolId) -> Self {
use Nir::*;
match self {

View File

@ -102,6 +102,7 @@ use memchr::memchr2;
use super::{Nir, NirEntity};
use crate::{
diagnose::{panic::DiagnosticPanic, Annotate, AnnotatedSpan, Diagnostic},
f::Functor,
fmt::{DisplayWrapper, TtQuote},
parse::{prelude::*, util::SPair, NoContext},
span::Span,

View File

@ -28,7 +28,7 @@
pub mod expand;
use super::{prelude::*, state::TransitionData};
use crate::{span::Span, sym::SymbolId};
use crate::{f::Functor, span::Span, sym::SymbolId};
use std::fmt::Display;
/// A [`SymbolId`] with a corresponding [`Span`].
@ -67,7 +67,9 @@ impl SPair {
Self(sym, _) => *sym,
}
}
}
impl Functor<SymbolId> for SPair {
/// Map over the [`SymbolId`] of the pair while retaining the original
/// associated [`Span`].
///
@ -75,19 +77,21 @@ impl SPair {
/// code the user entered,
/// since diagnostic messages will reference the original source
/// location that the modification was derived from.
pub fn map(self, f: impl FnOnce(SymbolId) -> SymbolId) -> Self {
fn map(self, f: impl FnOnce(SymbolId) -> SymbolId) -> Self {
match self {
Self(sym, span) => Self(f(sym), span),
}
}
}
impl Functor<Span> for SPair {
/// Map over the [`Span`] of the pair while retaining the associated
/// [`SymbolId`].
///
/// This operation is useful,
/// for example,
/// when resolving or overriding identifiers.
pub fn map_span(self, f: impl FnOnce(Span) -> Span) -> Self {
fn map(self, f: impl FnOnce(Span) -> Span) -> Self {
match self {
Self(sym, span) => Self(sym, f(span)),
}