1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
//! This example demonstrates differences between a *static dispatch* and
//! *dynamic dispatch* of method calls.
//!
//! Second point this example makes is that polymorphism without inheritance (i.e. other forms of
//! polymorphism other than subclass polymorphism) is not a matter of static or dynamic dispatch.
//!
//! Notice that in both examples, that is `gradient_descent_static` and `gradient_descent_dynamic`,
//! there is no inheritance hierarchy and relation between [Quadratic] (a `struct`) and
//! [Trigonometric] (an `enum`). The only link between them the typeclass [Differentiable] which
//! encapsulates common behavior of types in the class of differentiable functions and makes both
//! GD functions mentioned above polymorphic over this behavior.
use std::boxed::Box;

/// Interface of a real 1D differentiable function
pub trait Differentiable {
    /// Compute the first derivative of this function at given point `x`
    fn grad(&self, x: f64) -> f64;
}

#[allow(dead_code)]
pub struct Quadratic {
    a: f64,
    b: f64,
    c: f64,
}

impl Quadratic {
    #[inline(always)]
    pub fn stack_alloc(a: f64, b: f64, c: f64) -> Self {
        Self { a, b, c }
    }

    #[inline(always)]
    pub fn heap_alloc(a: f64, b: f64, c: f64) -> Box<Self> {
        Box::new(Self { a, b, c })
    }
}

impl Differentiable for Quadratic {
    #[inline(always)]
    fn grad(&self, x: f64) -> f64 {
        2. * self.a * x - self.b
    }
}

pub enum Trigonometric {
    Sine,
    Cosine,
}

impl Differentiable for Trigonometric {
    #[inline(always)]
    fn grad(&self, x: f64) -> f64 {
        match self {
            Trigonometric::Sine => x.cos(),
            Trigonometric::Cosine => -x.sin(),
        }
    }
}

/// Gradient Descent that finds a minimum of a statically defined function `f` on given `interval`.
///
/// Static dispatch means that this function i *monomorphized* and thus the type of `f` is known at
/// compilation time. Monomorphization means that the compiler duplicates this function for every
/// type implementing `Differentiable` and for each copy substitutes the concrete type for the
/// generic parameter `F`.
///
/// Call `f.grad(x)` is direct and the method address is explicitly mentioned in the assembly code.
/// Furthermore, trait implementations can benefit from inlining.
///
/// There's, however, also a disadvantage - one can't put different implementations into let's say a
/// collection (e.g. `Vec`) that expects homogeneous types. Such a structure need a different kind \
/// of polymorphism which is provided by dynamic dispatch.
pub fn gradient_descent_static<F>(f: &F, max_iters: usize, eta: f64) -> f64
where
    F: Differentiable,
{
    let mut x = 0.0;
    for _ in 0..max_iters {
        // Note that `F::grad(f, x)` works as well due to static dispatch and monomorphization.
        x -= eta * f.grad(x);
    }
    x
}

/// Gradient Descent that finds a minimum of a dynamically defined function `f` on given `interval`.
///
/// Dynamic dispatch means that the actual type of `f` (or more precisely of the `grad` method) is
/// determined at runtime. Call `f.grad(x)` is indirect via a *vtable* - a lookup table that holds
/// memory addresses of `grad` methods for each type implementing `Differentiable`.
///
/// Contrary to the statically dispatched version, this implementation `f` only provides the public
/// interface defined by `Differentiable` trait. Moreover any inlining on `dyn` traits is ignored.
pub fn gradient_descent_dynamic(f: &dyn Differentiable, max_iters: usize, eta: f64) -> f64 {
    let mut x = 0.0;
    for _ in 0..max_iters {
        x -= eta * f.grad(x);
    }
    x
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::f64::consts::FRAC_PI_2;

    const EPS: f64 = 0.00001;

    macro_rules! assert_delta {
        ($x:expr, $y:expr, $d:expr) => {
            if !($x - $y < $d || $y - $x < $d) {
                panic!("{} is not within {} of {}", $x, $d, $y);
            }
        };
    }

    #[test]
    fn quadratic() {
        // min { 2*x^2 - x } = -1/8 at x = 1/4
        let function = Quadratic {
            a: 2.,
            b: 1.,
            c: 0.,
        };

        // Minimize for 10k iterations with step size 0.01

        // Test GD with static dispatch
        let x_min = gradient_descent_static(&function, 10_000, 0.01);
        assert_delta!(0.25, x_min, EPS);

        // Test GD with dynamic dispatch
        let function = Box::new(function);
        let x_min = gradient_descent_dynamic(function.as_ref(), 10_000, 0.01);
        assert_delta!(0.25, x_min, EPS);
    }

    #[test]
    fn trigonometric() {
        let function = Trigonometric::Sine;

        // Minimize for 10k iterations with step size 0.01

        // Test GD with static dispatch
        let x_min = gradient_descent_static(&function, 10_000, 0.01);
        assert_delta!(FRAC_PI_2, x_min, EPS);

        // Test GD with dynamic dispatch
        let function = Box::new(function);
        let x_min = gradient_descent_dynamic(function.as_ref(), 10_000, 0.01);
        assert_delta!(FRAC_PI_2, x_min, EPS);
    }

    #[test]
    fn dynamic_polymorphism() {
        // Define a collection of `Differentiable` functions that are heap-allocated
        //  - See [CannotMonomorphizeDifferentiableInVecTest]
        let functions: Vec<Box<dyn Differentiable>> = vec![
            Box::new(Trigonometric::Sine),
            Box::new(Trigonometric::Cosine),
        ];

        // Test that GD with dynamic dispatch works
        for function in functions.into_iter() {
            gradient_descent_dynamic(function.as_ref(), 10_000, 0.01);
        }
    }
}

/// This test shows that if one wants to construct a container ([Vec] in this case) of
/// [Differentiable] instances, it cannot be done with a *static polymorphic type*.
///
/// # Example
/// ```compile_fail
/// use rust_examples::dispatch::{Differentiable, Trigonometric};
///
/// let _: Vec<Differentiable> = vec![Trigonometric::Sine, Trigonometric::Cosine];
/// ```
///
/// The *size* of this heterogeneous container cannot be known at compile time - this issue is that
/// the compiler tries to monomorphize [Differentiable] items in the [Vec] but each one might be
/// different and the collection is dynamic, hence the unknown size.
///
/// In consequence, here one **must** use heap-allocated `dyn` instances (i.e. dynamic dispatch)!
pub struct CannotMonomorphizeDifferentiableInVecTest;