Skip to content

Commit

Permalink
Doc changes and add tutorial section w/ contrived example
Browse files Browse the repository at this point in the history
  • Loading branch information
sdleffler committed Apr 13, 2021
1 parent 82c6b10 commit e7ad880
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 11 deletions.
2 changes: 1 addition & 1 deletion dialectic/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ readme = "../README.md"

[dependencies]
thiserror = "1"
call-by = { version = "^0.2.3" }
call-by = { version = "^0.2.3", path = "../../call-by" }
vesta = { version = "0.1" }
tokio = { version = "1", optional = true }
tokio-util = { version = "0.6", features = ["codec"], optional = true }
Expand Down
23 changes: 13 additions & 10 deletions dialectic/src/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@
//! receiving channel `Rx`. In order to use a `Chan` to run a session, these underlying channels
//! must implement the traits [`Transmitter`] and [`Receiver`], as well as [`Transmit<T>`](Transmit)
//! and [`Receive<T>`](Receive) for at least the types `T` used in those capacities in any given
//! session.
//! session. If you want to use [`Choice<N>`] to carry your branching messages, you will also need
//! [`TransmitChoice`] and [`ReceiveChoice`]. These two pairs of traits are what need to be
//! implemented for a transport backend to function with Dialectic; if a custom backend for an
//! existing protocol is being implemented, [`TransmitChoice`] and [`ReceiveChoice`] do not
//! necessarily need to be implemented and can be left out as necessary/desired.
//!
//! Functions which are generic over their backend will in turn need to specify the bounds
//! [`Transmit<T>`](Transmit) and [`Receive<T>`](Receive) for all `T`s they send and receive,
Expand All @@ -26,8 +30,9 @@ pub use choice::*;

/// A backend transport used for transmitting (i.e. the `Tx` parameter of [`Chan`](crate::Chan))
/// must implement [`Transmitter`], which specifies what type of errors it might return, as well as
/// giving a method to send [`Choice`]s across the channel. This is a super-trait of [`Transmit`],
/// which is what's actually needed to receive particular values over a [`Chan`](crate::Chan).
/// giving a method to send [`Choice`]s across the channel. This is a super-trait of [`Transmit`]
/// and [`TransmitChoice`], which are what's actually needed to receive particular values over a
/// [`Chan`](crate::Chan).
///
/// If you're writing a function and need a lot of different [`Transmit<T>`](Transmit) bounds, the
/// [`Transmitter`](macro@crate::Transmitter) attribute macro can help you specify them more
Expand Down Expand Up @@ -212,8 +217,8 @@ impl<Tx: Transmitter + Transmit<T>, T: Match + Transmittable, const N: usize>

/// A backend transport used for receiving (i.e. the `Rx` parameter of [`Chan`](crate::Chan)) must
/// implement [`Receiver`], which specifies what type of errors it might return. This is a
/// super-trait of [`Receive`] and [`ReceiveCase`], which are the traits that are actually needed to
/// receive particular values over a [`Chan`](crate::Chan).
/// super-trait of [`Receive`] and [`ReceiveChoice`], which are the traits that are actually needed
/// to receive particular values over a [`Chan`](crate::Chan).
///
/// If you're writing a function and need a lot of different [`Receive<T>`](Receive) bounds, the
/// [`Receiver`](macro@crate::Receiver) attribute macro can help you specify them more succinctly.
Expand Down Expand Up @@ -264,13 +269,11 @@ pub trait ReceiveChoice: Receiver {
/// [`vesta`](https://docs.rs/vesta) crate's [`Match`] and [`Case`] traits, which can be derived for
/// some types through Vesta's `Match` derive macro.
///
/// If you are writing a backend for a new protocol, you almost certainly do not need to implement
/// [`ReceiveCase`] for your backend, and you can rely on an implementation of [`ReceiveChoice`]. If
/// you are writing a backend for an existing protocol which you are reimplementing using Dialectic,
/// however, then [`ReceiveCase`] is necessary to allow you to branch on a custom type.
///
/// If you're writing a function and need a lot of different `ReceiveCase<T, C>` bounds, the
/// [`Receiver`](macro@crate::Receiver) attribute macro can help you specify them more succinctly.
///
/// You do not ever need to implement `ReceiveCase` yourself. Blanket implementations are provided
/// for all types that matter, and rely on `ReceiveChoice` and `Receive<T>`.
pub trait ReceiveCase<T>: Receiver
where
T: Match,
Expand Down
2 changes: 2 additions & 0 deletions dialectic/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,4 +185,6 @@ pub mod prelude {
pub use call_by::{Mut, Ref, Val};
#[doc(no_inline)]
pub use dialectic_macro::{offer, Receiver, Session, Transmitter};
#[doc(no_inline)]
pub use vesta::Match;
}
76 changes: 76 additions & 0 deletions dialectic/src/tutorial.rs
Original file line number Diff line number Diff line change
Expand Up @@ -808,6 +808,82 @@ macros, see their documentation. Additionally, the code in the
is written to be backend-agnostic, and uses these attributes. They may prove an additional resource
if you get stuck.
# Custom choice "carrier" types
Under the hood, Dialectic implements sending/receiving choices from the `offer` and `choose`
constructs using a special type called `Choice<N>`. Usually, this entails sending/receiving a
single byte; however, if you're implementing an existing protocol, you may want to branch on
something else and sending/receiving a single byte may not make any sense at all. This is where the
Vesta crate and its `Match` derive trait and macro come in; the [`offer!`] macro is implemented
under the hood using Vesta's `case!` macro, and accepts the same syntax for pattern matching. This
is mostly useful for when you're writing your own backend and need specific behavior for
transmitting/receiving messages.
As a *very* contrived example, we could rewrite the `QuerySum` example from the branching example
using `Option` to carry our decisions:
```
use dialectic::prelude::*;
use dialectic_tokio_mpsc as mpsc;
type QuerySum = Session! {
loop {
choose Option<i64> {
0 => { // None
recv i64;
break;
}
1 => continue, // Some(i64), i64 is implicitly sent (and received on the other side)
}
}
};
# #[tokio::main]
# async fn main() -> Result<(), Box<dyn std::error::Error>> {
#
let (mut c1, mut c2) = QuerySum::channel(|| mpsc::channel(1));
// Sum all the numbers sent over the channel
tokio::spawn(async move {
let mut sum = 0i64;
let c2 = loop {
c2 = offer!(in c2 {
0 => break c2,
1(n) => {
sum += n;
c2
},
})?;
};
c2.send(sum).await?.close();
Ok::<_, mpsc::Error>(())
});
// Send some numbers to be summed
for n in 0..=10 {
c1 = c1.choose::<1>(n).await?;
}
// Get the sum
let (sum, c1) = c1.choose::<0>(()).await?.recv().await?;
c1.close();
assert_eq!(sum, 55);
# Ok(())
# }
```
In real code you probably wouldn't want to do this since using something other than `Option` or
just using the default `Choice<N>` type would be fine and way more explicit; `Match` depends on the
order in which you define enum fields, so it can get a bit messy if it's not well-defined which
index is which. However, defining your own `Match` instance for a type is as simple as `#[derive
(Match)]`, and `Match` is re-exported from the Dialectic prelude module, streamlining as much of
the process as possible.
Unfortunately, using actual enum identifiers for indices is highly nontrivial. This construct is
roughly equivalent to a dependent type, and while we'd like to eventually have named indices in the
future, it's sufficient for now to just be able to do this kind of dependent pattern matching at
all.
# Wrapping up
We've now finished our tour of everything you need to get started programming with Dialectic! ✨
Expand Down

0 comments on commit e7ad880

Please sign in to comment.