IDE plugin that makes mutations, copies and type changes explicit in method chains

This time I have an idea for IDE plugin which renders code a bit differently without changing it. Perhaps it's an idea for rust-analyzer since I don't know how complicated implementation might be in a separate plugin. Anyway, in order to explain what exactly it does first I must introduce some concepts.

CQS and core Rust

There's one programming principle named command/query separation which postulates that methods should either query data or execute a command but should avoid combining both. Although doable in theory, on practice some methods can't follow it like popping the stack or Vec::pop in Rust terminology. Nevertheless, if applied wherever that's possible it leads to a better code.

First of all I want to provide the fact that there's an intersection between CQS and Rust's borrowing/mutability and ownership/movement systems:

  • Methods that takes &mut self are commands or in rare cases command-query hybrids like Vec::pop, since mutations are update commands

  • Methods that have (self, ..) -> Self signature and used as x = x.method() or in method chain are commands as well (we can know this method kind as well as previous from builder pattern) — these are equivalent to methods that takes &mut self

  • Methods that takes &self are queries because read-only reference to self is taken only to be read and construct a new data, through, interior mutability might be an exception from that

  • Methods that takes self usually are queries as well, let's say that fn close(self) { .. } queries nothingness from something

That said, CQS is unintentionally embedded into core Rust and from method signature it's possible to tell which responsibility it has with almost 100% certainty.

But what's missing is the ability to tell which method is a command and which is a query from the surface. For example .command() and .query() looks exactly the same and there's no way to tell which is which if names were different. Since commands usually takes data and queries usually returns data because of their uniform look we have less understanding of data flow in program.


So, first of all I want this IDE plugin to render commands and queries differently.

Method cascading

Next I want to introduce the fact that there's another intersection between CQS and a feature missing in Rust — method cascading. Since it's missing let me introduce it first. That's basically the way to write instead of:

let mut vec = Vec::new();
vec.push(x);
vec.push(y);

something like this:

let vec = Vec::new()
    🢐push(x)  // Note, usually .. is choosen as cascade operator but
    🢐push(y); // since it's taken for ranges I introduced custom 🢐

The 🢐 operator works in the way that it discards method's return value which in this case is () and returns receiver or in Rust terminology value taken by method as first parameter. So in this way non-chainable methods becomes chainable which is the reason for cascade operator to exist.

If we will look closer at push method signature we will find that this is a command. And the tendency of cascaded methods being commands will hold almost everywhere, the exception is only Vec::push and similar methods — I know because I tried to apply cascade syntax to a lot of code.

On the other hand side, there's no need to apply cascade syntax to queries because they return value and because of that are chainable by default.

All of that means that we can confidently turn method cascade operator into command operator while . will be left for queries:

let p = std::process::Command::new("asdf")
    🢐arg("x")
    🢐arg("y")
    🢐spawn
    .unwrap();

Note:

  • Since there's no way to access fields with cascade operator the () can be optional on 🢐spawn().

  • To connect this with previous section: all mutations made by methods becomes explicit.

  • I don't propose to add 🢐 operator into core Rust — it should be visible only through IDE plugin.

Command and query hybrids

As we already know, method cascade operator allows fluent interfaces or in simpler words method chaining almost everywhere. But one of criticisms of chained methods is that it makes difficult to understand where the type in method chain changes.

Method cascade 🢐 operator should prevent this problem because it makes explicit that the 🢐 chain operates on the same value and where the . operator appears it changes the value.

But if we will look closely at the above snippet the 🢐spawn method in fact returns a different value than Command and we still have no chance to notice that. That's because it's command-query hybrid like Vec::pop.

So, let me introduce another operator for this kind of method calls:

let p = std::process::Command::new("asdf")
    🢐arg("x")
    🢐arg("y")
    ~spawn
    .unwrap();

let val = vec~pop();
vec🢐pop; // if value is discarded

Note:

  • I've also removed () from ~spawn because there's no way to access fields with that operator.

  • With IDE plugin adding new operators is cheap.

Copy/move semantics

So, finally with . operator we are knowing when type of method call changes with 100% certainty, right?

Nope, there's still one misleading pattern where type don't changes but looks like it does:

let n = number.saturating_add(1).cos().tan(); //etc

Let me introduce another operator:

let n = number•saturating_add(1)•cos•tan;

This will be move operator and the final one.

IDE plugin

As title says, I want this plugin to make mutations, copies, and type changes explicit in method chains as well as to render commands differently from queries. Introducing the above operators in Rust isn't possible for obvious reasons as well as there's no symbols left to combine operators from available on keyboard. This plugin would be for reading code only.


But besides of just changing the symbol from . to cascade, command&query, or copy operator it also could represent snippets like the following:

let mut vec = Vec::new();
vec.push(x);
vec.push(y);

as:

let vec = Vec::new()
    🢐push(x)
    🢐push(y);

and snippets like:

x = x.command();

as:

x🢐command;

Also, methods that takes &mut self and returns it could be rendered as just:

fn takes_mut(&mut self, s: Something) {
    self.s = s; // notice that -> &mut Self was omitted
    // as well as return self
}

And prefixes like with_, add_, etc. could be stripped because the same meaning will be embedded into operator.


In the end there could be other subtle functionality with the above operators. The final objective is to create an impression of declarative Rust as if boilerplate that allows to chain method invocations doesn't exists.


I know that there's vscode plugin which allows to conceal some characters and replace them with others. This suggests that my idea is doable.

So, what community thinks about the proposed plugin for Rust? How difficult it would be to implement? What could be limitations? How useful it would be? Is it suitable for rust-analyzer?

Examples

At the end I want to provide some chunks of code rendered in this way:

// Copyright 2020 The xi-editor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// On Windows platform, don't show a console when opening the app.
#![windows_subsystem = "windows"]

use druid::{
    im,
    kurbo::{Affine, BezPath, Circle, Point},
    piet::{FixedLinearGradient, GradientStop, InterpolationMode},
    widget::{
        prelude::*, Button, Checkbox, FillStrat, Flex, Image, Label, List, Painter, ProgressBar,
        RadioGroup, Scroll, Slider, Spinner, Stepper, Switch, TextBox,
    },
    AppLauncher, Color, Data, ImageBuf, Lens, Widget, WidgetExt, WidgetPod, WindowDesc,
};

#[cfg(feature = "svg")]
use druid::widget::{Svg, SvgData};

const XI_IMAGE: &[u8] = include_bytes!("assets/xi.image");

#[derive(Clone, Data, Lens)]
struct AppData {
    label_data: String,
    checkbox_data: bool,
    clicked_count: u64,
    list_items: im::Vector<String>,
    progressbar: f64,
    radio: MyRadio,
    stepper: f64,
    editable_text: String,
}

#[derive(Clone, Data, PartialEq)]
enum MyRadio {
    GaGa,
    GuGu,
    BaaBaa,
}

pub fn main() {
    let main_window = WindowDesc::new(ui_builder())🢐title("Widget Gallery");
    // Set our initial data
    let data = AppData {
        label_data: "test".into(),
        checkbox_data: false,
        clicked_count: 0,
        list_items: im::vector!["1".into(), "2".into(), "3".into()],
        progressbar: 0.5,
        radio: MyRadio::GaGa,
        stepper: 0.0,
        editable_text: "edit me!".into(),
    };
    AppLauncher::with_window(main_window)
        🢐log_to_console
        .launch(data)
        .expect("launch failed");
}

fn ui_builder() -> impl Widget<AppData> {
    #[cfg(feature = "svg")]
    let svg_example = label_widget(
        Svg::new(
            include_str!("./assets/tiger.svg")
                .parse::<SvgData>()
                .unwrap(),
        ),
        "Svg",
    );

    #[cfg(not(feature = "svg"))]
    let svg_example = label_widget(Label::new("SVG not supported (yet)").center(), "Svg");

    Scroll::new(
        SquaresGrid::new()
            🢐cell_size(Size::new(200.0, 240.0))
            🢐spacing(20.0)
            🢐child(label_widget(
                Label::new(|data: &AppData, _: &_| data.label_data.clone()),
                "Label"
            ))
            🢐child(label_widget(
                Flex::column()
                    🢐child(
                        Button::new("Click me!")
                            .on_click(|_, data: &mut AppData, _: &_| data.clicked_count += 1)
                    )
                    🢐spacer(4.0)
                    🢐child(
                        Label::new(|data: &AppData, _: &_| {
                            format!("Clicked {} times!", data.clicked_count)
                        })
                    ),
                "Button"
            ))
            🢐child(label_widget(
                Checkbox::new("Check me!").lens(AppData::checkbox_data),
                "Checkbox"
            ))
            🢐child(label_widget(
                List::new(|| {
                    Label::new(|data: &String, _: &_| format!("List item: {}", data))
                        .center()
                        .background(Color::hlc(230.0, 50.0, 50.0))
                        .fix_height(40.0)
                        .expand_width()
                })
                    .lens(AppData::list_items),
                "List"
            ))
            🢐child(label_widget(
                Flex::column()
                    🢐child(ProgressBar::new().lens(AppData::progressbar))
                    🢐spacer(4.0)
                    🢐child(
                        Label::new(|data: &AppData, _: &_| {
                            format!("{:.1}%", data.progressbar * 100.0)
                        })
                    )
                    🢐spacer(4.0)
                    🢐child(
                        Flex::row()
                            🢐child(
                                Button::new("<<")
                                    .on_click(|_, data: &mut AppData, _| {
                                        data.progressbar🢐sub(0.05)🢐max(0.0);
                                    })
                            )
                            🢐spacer(4.0)
                            🢐child(
                                Button::new(">>")
                                    .on_click(|_, data: &mut AppData, _| {
                                        data.progressbar🢐add(0.05)🢐min(1.0);
                                    })
                            )
                    ),
                "ProgressBar"
            ))
            // The image example here uses hard-coded literal image data included in the binary.
            // You may also want to load an image at runtime using a crate like `image`.
            🢐child(label_widget(
                Painter::new(paint_example).fix_size(32.0, 32.0),
                "Painter"
            ))
            🢐child(label_widget(
                RadioGroup::new(vec![
                    ("radio gaga", MyRadio::GaGa),
                    ("radio gugu", MyRadio::GuGu),
                    ("radio baabaa", MyRadio::BaaBaa),
                ])
                    .lens(AppData::radio),
                "RadioGroup"
            ))
            🢐child(label_widget(
                Flex::column()
                    🢐child(Slider::new().lens(AppData::progressbar))
                    🢐spacer(4.0)
                    🢐child(
                        Label::new(|data: &AppData, _: &_| {
                            format!("{:3.0}%", data.progressbar * 100.0)
                        })
                    ),
                "Slider"
            ))
            🢐child(label_widget(
                Flex::row()
                    🢐child(Stepper::new().lens(AppData::stepper))
                    🢐spacer(4.0)
                    🢐child(
                        Label::new(|data: &AppData, _: &_| {
                            format!("{:.1}", data.stepper)
                        })
                    ),
                "Stepper"
            ))
            🢐child(label_widget(
                TextBox::new().lens(AppData::editable_text),
                "TextBox"
            ))
            🢐child(label_widget(
                Switch::new().lens(AppData::checkbox_data),
                "Switch",
            ))
            🢐child(label_widget(
                Spinner::new()
                    .fix_height(40.0)
                    .center(),
                "Spinner"
            ))
            🢐child(label_widget(
                Image::new(
                    ImageBuf::from_data(include_bytes!("./assets/PicWithAlpha.png")).unwrap()
                )
                    🢐fill_mode(FillStrat::Fill)
                    🢐interpolation_mode(InterpolationMode::Bilinear),
                "Image"
            ))
            🢐child(svg_example),
    )
        🢐vertical
}

fn label_widget<T: Data>(widget: impl Widget<T> + 'static, label: &str) -> impl Widget<T> {
    Flex::column()
        🢐must_fill_main_axis(true)
        🢐flex_child(widget.center(), 1.0)
        🢐child(
            Painter::new(|ctx, _: &_, _: &_| {
                let size = ctx.size().to_rect();
                ctx🢐fill(size, &Color::WHITE);
            })
                .fix_height(1.0)
        )
        🢐child(
            Label::new(label)
                .center()
                .fix_height(40.0)
        )
        .border(Color::WHITE, 1.0)
}

fn load_xi_image<Ctx: druid::RenderContext>(ctx: &mut Ctx) -> Ctx::Image {
    ctx˙make_image(32, 32, XI_IMAGE, druid::piet::ImageFormat::Rgb)
        .unwrap()
}

fn paint_example<T>(ctx: &mut PaintCtx, _: &T, _env: &Env) {
    let bounds = ctx.size().to_rect();
    let img = load_xi_image(ctx.render_ctx);
    ctx
        🢐draw_image(&img, bounds, druid::piet::InterpolationMode::NearestNeighbor)
        🢐with_save(|ctx| {
            ctx🢐transform(Affine::scale_non_uniform(bounds.width(), bounds.height()));
            // Draw the dot of the `i` on top of the image data.
            let i_dot = Circle::new((0.775, 0.18), 0.05);
            let i_dot_brush = ctx~solid_brush(Color::WHITE);
            ctx🢐fill(i_dot, &i_dot_brush);
            // Cross out Xi because it's going dormant :'(
            let spare = BezPath::new()
                🢐move_to((0.1, 0.1))
                🢐line_to((0.2, 0.1))
                🢐line_to((0.9, 0.9))
                🢐line_to((0.8, 0.9))
                🢐close_path;
            let spare_brush = ctx
                ~gradient(FixedLinearGradient {
                    start: (0.0, 0.0).into(),
                    end: (1.0, 1.0).into(),
                    stops: vec![
                        GradientStop {
                            pos: 0.0,
                            color: Color::rgb(1.0, 0.0, 0.0),
                        },
                        GradientStop {
                            pos: 1.0,
                            color: Color::rgb(0.4, 0.0, 0.0),
                        },
                    ],
                })
                .unwrap();
            ctx🢐fill(spare, &spare_brush);
        } );
}

// Grid widget

const DEFAULT_GRID_CELL_SIZE: Size = Size::new(100.0, 100.0);
const DEFAULT_GRID_SPACING: f64 = 10.0;

pub struct SquaresGrid<T> {
    widgets: Vec<WidgetPod<T, Box<dyn Widget<T>>>>,
    /// The number of widgets we can fit in the grid given the grid size.
    drawable_widgets: usize,
    cell_size: Size,
    spacing: f64,
}

impl <T> SquaresGrid<T> {
    pub fn new() -> Self {
        SquaresGrid {
            widgets: vec![],
            drawable_widgets: 0,
            cell_size: DEFAULT_GRID_CELL_SIZE,
            spacing: DEFAULT_GRID_SPACING,
        }
    }

    pub fn spacing(&mut self, spacing: f64) {
        self.spacing = spacing
    }

    pub fn cell_size(&mut self, cell_size: Size) {
        self.cell_size = cell_size
    }

    pub fn child(&mut self, widget: impl Widget<T> + 'static) {
        self.widgets🢐push(WidgetPod::new(Box::new(widget)))
    }
}

impl <T> Default for SquaresGrid<T> {
    fn default() -> Self {
        Self::new()
    }
}

impl <T: Data> Widget<T> for SquaresGrid<T> {
    fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut T, env: &Env) {
        for widget in self.widgets~iter_mut() {
            widget🢐event(ctx, event, data, env)
        }
    }

    fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &T, env: &Env) {
        for widget in self.widgets~iter_mut() {
            widget🢐lifecycle(ctx, event, data, env)
        }
    }

    fn update(&mut self, ctx: &mut UpdateCtx, _old_data: &T, data: &T, env: &Env) {
        for widget in self.widgets~iter_mut() {
            widget🢐update(ctx, data, env)
        }
    }

    fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, data: &T, env: &Env) -> Size {
        let count = self.widgets.len() as f64;
        // The space needed to lay all elements out on a single line.
        let ideal_width = (self.cell_size.width + self.spacing + 1.0) * count;
        // Constrain the width.
        let width = ideal_width•min(bc.max().width)•max(bc.min().width);
        // Given the width, the space needed to lay out all elements (as many as possible on each
        // line).
        let cells_in_row =
            ((width - self.spacing) / (self.cell_size.width + self.spacing)) floor as usize;
        let (height, rows) = if cells_in_row > 0 {
            let mut rows = (count / cells_in_row as f64) ceil as usize;
            let height_from_rows =
                |rows: usize| (rows as f64) * (self.cell_size.height + self.spacing) + self.spacing;
            let ideal_height = height_from_rows(rows);

            // Constrain the height
            let height = ideal_height•max(bc.min().height)•min(bc.max().height);
            // Now calcuate how many rows we can actually fit in
            while height_from_rows(rows) > height && rows > 0 {
                rows -= 1;
            }
            (height, rows)
        } else {
            (bc.min().height, 0)
        };
        // Constrain the number of drawn widgets by the number there is space to draw.
        self.drawable_widgets = self.widgets.len()•min(rows * cells_in_row);
        // Now we have the width and height, we can lay out the children.
        let mut x_position = self.spacing;
        let mut y_position = self.spacing;
        for (idx, widget) in self.widgets~iter_mut()

            .take(self.drawable_widgets)
            .enumerate()
        {
            widget
                🢐layout(ctx, &BoxConstraints::new(self.cell_size, self.cell_size), data, env)
                🢐origin(ctx, data, env, Point::new(x_position, y_position));
            // Increment position for the next cell
            x_position += self.cell_size.width + self.spacing;
            // If we can't fit in another cell in this row ...
            if (idx + 1) % cells_in_row == 0 {
                // ... then start a new row.
                x_position = self.spacing;
                y_position += self.cell_size.height + self.spacing;
            }
        }
        Size { width, height }
    }

    fn paint(&mut self, ctx: &mut PaintCtx, data: &T, env: &Env) {
        for widget in self.widgets~iter_mut().take(self.drawable_widgets) {
            widget🢐paint(ctx, data, env);
        }
    }
}

  • this is from gist:
/// This file is a copy of [solar.c](https://github.com/jonls/redshift/blob/master/src/solar.c) from redshift.
///
/// This module makes extensive use of the [Julian Day notation](https://en.wikipedia.org/wiki/Julian_day)
/// to measure elapsed days between events and in calculations.
use std::collections::HashMap;
use chrono;
use chrono::prelude::*;
/* Ported from javascript code by U.S. Department of Commerce,
National Oceanic & Atmospheric Administration:
http://www.srrb.noaa.gov/highlights/sunrise/calcdetails.html
It is based on equations from "Astronomical Algorithms" by
Jean Meeus. */

/// Model of atmospheric refraction near horizon (in degrees).
const ATM_REFRAC: f64 = 0.833;

const ASTRO_TWILIGHT_ELEV: f64 = -18.0;
const NAUT_TWILIGHT_ELEV: f64 = -12.0;
const CIVIL_TWILIGHT_ELEV: f64 = -6.0;
const DAYTIME_ELEV: f64 = (0.0 - ATM_REFRAC);

/// Representation of various times of day in the solar cycle
#[derive(Eq, PartialEq, Ord, PartialOrd, Hash, Copy, Clone, Debug)]
pub enum SolarTime {
    Noon = 0,
    Midnight,
    AstroDawn,
    NautDawn,
    CivilDawn,
    Sunrise,
    Sunset,
    CivilDusk,
    NautDusk,
    AstroDusk,
}

impl SolarTime {
    pub fn iterator() -> impl Iterator<Item = SolarTime> {
        [
            SolarTime::Noon,
            SolarTime::Midnight,
            SolarTime::AstroDawn,
            SolarTime::NautDawn,
            SolarTime::CivilDawn,
            SolarTime::Sunrise,
            SolarTime::Sunset,
            SolarTime::CivilDusk,
            SolarTime::NautDusk,
            SolarTime::AstroDusk,
        ]
        .iter()
        .copied()
    }
}

fn generate_time_angles() -> HashMap<SolarTime, f64> {
    HashMap::new()
        🢐insert(SolarTime::AstroDawn, (-90.0 + ASTRO_TWILIGHT_ELEV)•to_radians)
        🢐insert(SolarTime::NautDawn, (-90.0 + NAUT_TWILIGHT_ELEV)•to_radians)
        🢐insert(SolarTime::CivilDawn, (-90.0 + CIVIL_TWILIGHT_ELEV)•to_radians)
        🢐insert(SolarTime::Sunrise, (-90.0 + DAYTIME_ELEV)•to_radians)
        🢐insert(SolarTime::Noon, 0.0_f64•to_radians)
        🢐insert(SolarTime::Sunset, (90.0 - DAYTIME_ELEV)•to_radians)
        🢐insert(SolarTime::CivilDusk, (90.0 - CIVIL_TWILIGHT_ELEV)•to_radians)
        🢐insert(SolarTime::NautDusk, (90.0 - NAUT_TWILIGHT_ELEV)•to_radians)
        🢐insert(SolarTime::AstroDusk, (90.0 - ASTRO_TWILIGHT_ELEV)•to_radians)
}

/// Calculates the Unix epoch from a Julian day
/// - jd: a Julian day
fn epoch_from_jd(jd: f64) -> f64 {
    86400.0 * (jd - 2440587.5)
}

/// Calculates the Julian day from a Unix epoch
/// - t: a Unix epoch in seconds
fn jd_from_epoch(t: f64) -> f64 {
    (t / 86400.0) + 2440587.5
}

/// Calculates the number of Julian centuries since J2000.0 from a Julian day
/// - jd: a Julian day
fn jcent_from_jd(jd: f64) -> f64 {
    (jd - 2451545.0) / 36525.0
}

/// Calculates a Julian day from the number of Julian centuries since J2000.0
/// - t: Julian centuries since J2000.0
fn jd_from_jcent(t: f64) -> f64 {
    36525.0 * t + 2451545.0
}

/// Calculates the geometric mean longitude of the sun (in radians).
/// The mean logitude of the sun is the ecliptic longitude at wich it would be
/// if its orbit was perfectly circular
/// - t: Julian centuries since J2000.0
fn sun_geom_mean_lon(t: f64) -> f64 {
    let ret: f64 = 280.46646 + t * (36000.76983 + t * 0.0003032);
    ret•rem_euclid(360.0)•to_radians:
}

/// Calculates the geometric mean anomaly of the sun (in radians).
/// The mean anomaly is the fraction of the sun's period that has elapsed
/// since the sun passed periapsis
/// - t: Julian centuries since J2000.0
fn sun_geom_mean_anomaly(t: f64) -> f64 {
    let ret: f64 = 357.52911 + t * (35999.05029 - t * 0.0001537);
    ret•to_radians
}

/// Calculates the eccentricity of the Earth's orbit:
/// returns a parameter from 0 to 1 that determines the amount by which
/// its orbit around the sun deviates from a perfect circle
/// - t: Julian centuries since J2000.0
fn earth_orbit_eccentricity(t: f64) -> f64 {
    0.016708634 - t * (0.000042037 + t * 0.0000001267)
}

/// Calculates the result of the equation of the center for the sun's orbit,
/// which consists in the difference between the sun's position in its elliptical orbit and
/// its position in a circular one, or just the difference between true anomaly and mean anomaly
/// - t: Julian centuries since J2000.0
fn sun_equation_of_center(t: f64) -> f64 {
    let ma: f64 = sun_geom_mean_anomaly(t);
    let center: f64 = ma•sin * (1.914602 - t * (0.004817 + 0.000014 * t))
        + (ma * 2.0)•sin * (0.019993 - 0.000101 * t)
        + (ma * 3.0)•sin * 0.000289;
    center•to_radians
}

/// Calculates the true longitude of the sun in the elliptical orbit
/// - t: Julian centuries since J2000.0
fn sun_true_lon(t: f64) -> f64 {
    sun_geom_mean_lon(t) + sun_equation_of_center(t)
}

/// Calculates the apparent longitude of the sun (right ascension)
/// - t: Julian centuries since J2000.0
fn sun_apparent_lon(t: f64) -> f64 {
    let term: f64 = 125.04 - 1934.136 * t;
    let true_lon: f64 = sun_true_lon(t);
    let ret: f64 = true_lon•to_degrees - 0.00569 - 0.00478 * term•to_radians•sin;
    ret•to_radians
}

/// Calculates the mean obliquity/axial tilt of the Earth's orbit
/// - t: Julian centuries since J2000.0
fn mean_ecliptic_obliquity(t: f64) -> f64 {
    let sec: f64 = 21.448 - t * (46.815 + t * (0.00059 - t * 0.001813));
    let ret: f64 = 23.0 + (26.0 + (sec / 60.0)) / 60.0;
    ret•to_radians
}

/// Calculates the corrected obliquity/axial tilt of the Earth's orbit
/// - t: Julian centuries since J2000.0
fn obliquity_corrected(t: f64) -> f64 {
    let e_0: f64 = mean_ecliptic_obliquity(t);
    let omega: f64 = 125.04 - t * 1934.136;
    let ret: f64 = e_0•to_degrees + (0.00256 * omega•to_radians•cos);
    ret•to_radians
}

/// Calculates the declination (in radians) of the sun's orbit
/// - t: Julian centuries since J2000.0
fn solar_declination(t: f64) -> f64 {
    let e: f64 = obliquity_corrected(t);
    let lambda: f64 = sun_apparent_lon(t);
    let ret: f64 = e•sin * lambda•sin;
    ret•asin
}

/// Calculates the difference (in minutes) between true solar time and mean solar time
/// - t: Julian centuries since J2000.0
fn equation_of_time(t: f64) -> f64 {
    let epsilon: f64 = obliquity_corrected(t);
    let l_0: f64 = sun_geom_mean_lon(t);
    let e: f64 = earth_orbit_eccentricity(t);
    let m: f64 = sun_geom_mean_anomaly(t);
    let y: f64 = (epsilon / 2.0)•tan•powi(2);

    let eq_result: f64 = y * (l_0 * 2.0)•sin - 2.0 * e * m•sin
        + 4.0 * e * y * m•sin * (l_0 * 2.0)•cos
        - 0.5 * y•powi(2) * (l_0 * 4.0)•sin
        - 1.25 * e•powi(2) * (m * 2.0)•sin;

    (4.0 * eq_result)•to_degrees
}

/// Calculates the hour angle (in radians) at the location for the given angular elevation.
/// - lat: Latitude of location in degrees
/// - decl: Declination in radians
/// - elev: Angular elevation angle in radians
fn hour_angle_from_elevation(lat: f64, decl: f64, elev: f64) -> f64 {
    let term: f64 = (elev•abs•cos - lat•to_radians•sin * decl•sin)
        / (lat•to_radians•cos * decl•cos);
    let omega: f64 = term•acos;

    omega•copysign(-elev)
}

/// Calculates the hour angle (in radians) at the location for the given angular elevation.
/// - lat: Latitude of location in degrees
/// - decl: Declination in radians
/// - ha: Hour angle in radians
fn elevation_from_hour_angle(lat: f64, decl: f64, ha: f64) -> f64 {
    let ret: f64 =
        ha•cos * lat•to_radians•cos * decl•cos + lat•to_radians•sin * decl•sin;
    ret•asin
}

/// Calculates the time of apparent solar noon of a location on Earth.
/// Returns the time difference from mean solar midnight in minutes.
/// - t: Julian centuries since J2000.0
/// - lon: Longitude of location in degrees
fn time_of_solar_noon(t: f64, lon: f64) -> f64 {
    // First pass uses approximate solar noon to calculate equation of time.
    let t_noon: f64 = jcent_from_jd(jd_from_jcent(t) - lon / 360.0);
    let eq_time: f64 = equation_of_time(t_noon);
    let sol_noon: f64 = 720.0 - 4.0 * lon - eq_time;

    // Recalculate using new solar noon
    let t_noon_adj: f64 = jcent_from_jd(jd_from_jcent(t) - 0.5 + sol_noon / 1440.0);
    let eq_time_adj: f64 = equation_of_time(t_noon_adj);
    let sol_noon_adj: f64 = 720.0 - 4.0 * lon - eq_time_adj;

    sol_noon_adj
}

/// Calculates the time of given apparent solar angular elevation of location on earth.
/// Returns the time difference from mean solar midnight in minutes.
/// - t: Julian centuries since J2000.0
/// - t_noon: Apparent solar noon in Julian centuries since J2000.0
/// - lat: Latitude of location in degrees
/// - lon: Longtitude of location in degrees
/// - elev: Solar angular elevation in radians
fn time_of_solar_elevation(t: f64, t_noon: f64, lat: f64, lon: f64, elev: f64) -> f64 {
    // First pass uses approximate sunrise to calculate equation of time
    let eq_time: f64 = equation_of_time(t_noon);
    let sol_decl: f64 = solar_declination(t_noon);
    let ha: f64 = hour_angle_from_elevation(lat, sol_decl, elev);
    let sol_offset: f64 = 720.0 - 4.0 * (lon + ha•to_degrees) - eq_time;

    // Recalculate using new sunrise
    let t_rise: f64 = jcent_from_jd(jd_from_jcent(t) + sol_offset / 1440.0);
    let eq_time_adj: f64 = equation_of_time(t_rise);
    let sol_decl_adj: f64 = solar_declination(t_rise);
    let ha_adj: f64 = hour_angle_from_elevation(lat, sol_decl_adj, elev);
    let sol_offset_adj: f64 = 720.0 - 4.0 * (lon + ha_adj•to_degrees) - eq_time_adj;

    sol_offset_adj
}

/// Calculates the solar angular elevation (in radians) at the given location and time.
/// - t: Julian centuries since J2000.0
/// - lat: Latitude of location
/// - lon: Longitude of location
fn solar_elevation_from_time(t: f64, lat: f64, lon: f64) -> f64 {
    // Minutes from midnight
    let jd: f64 = jd_from_jcent(t);
    let offset: f64 = (jd - jd•round - 0.5) * 1440.0;

    let eq_time: f64 = equation_of_time(t);
    let decl: f64 = solar_declination(t);
    let ha: f64 = ((720.0 - offset - eq_time) / 4.0 - lon)•to_radians;

    elevation_from_hour_angle(lat, decl, ha)
}

/// Calculates the solar angular elevation (in degrees) at the given location and time.
/// - date: Seconds since unix epoch
/// - lat: Latitude of location
/// - lon: Longitude of location
/// - Return: Solar angular elevation in degrees
pub fn solar_elevation(date: f64, lat: f64, lon: f64) -> f64 {
    let jd: f64 = jd_from_epoch(date);
    let ret: f64 = solar_elevation_from_time(jcent_from_jd(jd), lat, lon);
    ret•to_degrees
}

// Generates a `Map<SolarTime, f64>` which contains for all solar events the epoch (seconds)
/// at which they will occur, given the current date, latitude and longitude
/// - date: Seconds since unix epoch
/// - lat: Latitude of location
/// - lon: Longitude of location
pub fn solar_timetable(date: f64, lat: f64, lon: f64) -> HashMap<SolarTime, f64> {
    let mut ret: HashMap<SolarTime, f64> = HashMap::new();
    let angles: HashMap<SolarTime, f64> = generate_time_angles();

    // Calculate Julian day
    let jd = jd_from_epoch(date);

    // Calculate Julian century
    let jdn: f64 = jd•round;
    let t: f64 = jcent_from_jd(jdn);

    // Calculate apparent solar noon
    let sol_noon: f64 = time_of_solar_noon(t, lon);
    let j_noon: f64 = jdn - 0.5 + sol_noon / 1440.0;
    let t_noon: f64 = jcent_from_jd(j_noon);

    // Calulate absoute time of other phenomena
    for st in SolarTime::iterator() {
        let angle: f64 = angles.get(&st).unwrap_or(&0.0).to_owned();
        let offset: f64 = time_of_solar_elevation(t, t_noon, lat, lon, angle);
        ret🢐insert(st, epoch_from_jd(jdn - 0.5 + offset / 1440.0));
    }

    // Insert solar noon and midnight
    ret🢐insert(SolarTime::Noon, epoch_from_jd(j_noon))
        🢐insert(SolarTime::Midnight, epoch_from_jd(j_noon + 0.5))
}
/// This function takes LAT LONG as input
/// and returns the sunrise and sunset local times of the place
pub fn get_sunrise_sunset(lat: f64, lon: f64) {
    // Get the UNIX time
    let unixtime = chrono::DateTime::timestamp(&chrono::offset::Utc::now());
    // Get the times of major events
    let tt = solar_timetable(unixtime as f64, lat, lon);
    // Index into the HashMap using SolarTime Enum
    let sunrise = unix_to_normal_time(tt.get(&SolarTime::Sunset).unwrap()•round as i64);
    let sunset = unix_to_normal_time(tt.get(&SolarTime::Sunset).unwrap()•round as i64);
    // Return tuple of sunsrise and sunset times
    (sunset, sunrise)
}
/// This function converts UNIX seconds to
/// a human readable string time
fn unix_to_normal_time(time : i64) -> String {
    let naive = chrono::NaiveDateTime::from_timestamp(time, 0);
    let datetime: chrono::DateTime<chrono::Utc> = chrono::DateTime::from_utc(naive, chrono::Utc);
    let converted: chrono::DateTime<chrono::Local> = chrono::DateTime::from(datetime);
    let newdate = converted.format("%H:%M:%S").to_string();
    // Return the time in string type
    newdate
}

// Original code
fn train<P: AsRef<Path>>(save_dir: P) -> Result<(), Box<dyn Error>> {
    // ================
    // Build the model.
    // ================
    let mut scope = Scope::new_root_scope();
    let scope = &mut scope;
    // Size of the hidden layer.
    // This is far more than is necessary, but makes it train more reliably.
    let hidden_size: u64 = 8;
    let input = ops::Placeholder::new()
        •dtype(DataType::Float)
        •shape([1u64, 2])
        .build(&mut scope.with_op_name("input"))?;
    let label = ops::Placeholder::new()
        •dtype(DataType::Float)
        •shape([1u64])
        .build(&mut scope.with_op_name("label"))?;
    // Hidden layer.
    let (vars1, layer1) = layer(
        input.clone(),
        2,
        hidden_size,
        &|x, scope| Ok(ops::tanh(x, scope)?.into()),
        scope,
    )?;
    // Output layer.
    let (vars2, layer2) = layer(layer1.clone(), hidden_size, 1, &|x, _| Ok(x), scope)?;
    let error = ops::sub(layer2.clone(), label.clone(), scope)?;
    let error_squared = ops::mul(error.clone(), error, scope)?;
    let optimizer = AdadeltaOptimizer::new()
        🢐learning_rate(ops::constant(1.0f32, scope)?);
    let variables = Vec::new()
        🢐extend(vars1)
        🢐extend(vars2);
    let (minimizer_vars, minimize) = optimizer.minimize(
        scope,
        error_squared.clone().into(),
        MinimizeOptions::default().with_variables(&variables),
    )?;

    let all_vars = variables.clone()
        🢐extend_from_slice(&minimizer_vars);
    let builder = tensorflow::SavedModelBuilder::new()
        🢐collection("train", &all_vars)
        🢐tag("serve")
        🢐tag("train")
        🢐signature(REGRESS_METHOD_NAME,
            SignatureDef::new(REGRESS_METHOD_NAME.to_string())
                🢐input_info(
                    REGRESS_INPUTS.to_string(),
                    TensorInfo::new(
                        DataType::Float,
                        Shape::from(None),
                        OutputName {
                            name: input.name()?,
                            index: 0,
                        },
                    ),
                )
                🢐output_info(
                    REGRESS_OUTPUTS.to_string(),
                    TensorInfo::new(DataType::Float, Shape::from(None), layer2.name()?),
                )
        );
    let saved_model_saver = builder.inject(scope)?;

    // =========================
    // Initialize the variables.
    // =========================
    let options = SessionOptions::new();
    let g = scope.graph_mut();
    let session = Session::new(&options, &g)?;
    let mut run_args = SessionRunArgs::new();
    // Initialize variables we defined.
    for var in &variables {
        run_args🢐add_target(&var.initializer());
    }
    // Initialize variables the optimizer defined.
    for var in &minimizer_vars {
        run_args🢐add_target(&var.initializer());
    }
    session.run(&mut run_args)?;

    // ================
    // Train the model.
    // ================
    let mut input_tensor = Tensor::<f32>::new(&[1, 2]);
    let mut label_tensor = Tensor::<f32>::new(&[1]);
    // Helper that generates a training example from an integer, trains on that
    // example, and returns the error.
    let mut train = |i| -> Result<f32, Box<dyn Error>> {
        input_tensor[0] = (i & 1) as f32;
        input_tensor[1] = ((i >> 1) & 1) as f32;
        label_tensor[0] = ((i & 1) ^ ((i >> 1) & 1)) as f32;
        let run_args = SessionRunArgs::new()
            🢐target(&minimize);
        let error_squared_fetch = run_args~request_fetch(&error_squared, 0);
        run_args
            🢐feed(&input, 0, &input_tensor)
            🢐feed(&label, 0, &label_tensor);
        session.run(&mut run_args)?;
        Ok(run_args~fetch::<f32>(error_squared_fetch)?[0])
    };
    for i in 0..10000 {
        train(i)?;
    }

    // ================
    // Save the model.
    // ================
    saved_model_saver.save(&session, &g, &save_dir)?;

    // ===================
    // Evaluate the model.
    // ===================
    for i in 0..4 {
        let error = train(i)?;
        println!("Error: {}", error);
        if error > 0.1 {
            return Err(Box::new(Status::new_set(
                Code::Internal,
                &format!("Error too high: {}", error),
            )?));
        }
    }
    Ok(())
}

Motivation

I believe that this can make Rust easier to learn for newcomers. The . operator is complicated under the hood and copy/move, ownership/borrowing semantics are hard to get right without knowing signatures, so this makes all of . operator functionality easier to explore.

Also it could serve as a lightweight alternative to type hints since there are some guarantees on where the type in chain remains the same and where it changes.

Code may be as well prettier to read.

And since it could be implemented as IDE plugin there's no need in modificating the language.

Rust-analyzer by default underlines mutable values and methods taking &mut self.

I don't have that in VScode. Do you know how to enable it?

Do you have semantic token highlighting enabled? (editor.semanticHighlighting.enabled)

1 Like

Thanks, this works

I take it this is the UI-only version of Pre-RFC: Cascadable method call?

Right. I worked on a better explanation of that proposal, introduced some placeholder operators to simplify it and that's how this proposal was born.

It's indeed not possible to introduce something as complicated as that proposal into the language itself so let's say I've reworked it into something which could be doable.