Motoko wishlist

After working with Motoko for a while I figured I would just make a list of my pain points and potential changes that would exist in my perfect world. If anyone knows alternatives that exist for the following, let me know below

  1. Problem - Error Propagation
    The number one issue that im running into is dealing with stopping code evaluation and returning an #error or continuing on with eval. My code is full of this:
let value = switch(doSomething(...)) {
  case (#error(e)) return #error(e);
  case (#ok(v)) v;
} 

Potential solution:
Have a built in Result<T, E> like Rust and handle propagation like null propagation with do ? {}

let result : Result<T, E> = do E {
  let value1 : T = doSomething(...)*;
  let value2 : T = doSomething(...)*;
  value2;
}
...
public func doSomething(...) : Result<T, E> {
...
}
  1. Problem - Multi/nested inline function calls
    A huge problem with readability and nesting is the need to perform multiple function calls on a value. Since motoko isn’t very object oriented, it requires many very verbose inline function calls
    Example:
let value : [Text] = Iter.toArray(Iter.sort(Iter.fromArray(Array.map([....], func (c) = ...))));

And that doesn’t include all the generic type names and function
Usually I just split these up into their own variables/lines to make it readable

Potential solution: Pipeline operators

let value : [Text] = [....]
  |> Array.map func(c) = ....
  |> Iter.fromArray
  |> Iter.sort
  |> Iter.toArray;
  1. Subtyping with pattern matching
    This is one I didn’t expect and is new to me with structural typing. I have run into this issue and I know others have as well.
    Usually ill have a Supertype that adds additional functionality on type of an existing type. When this happens I want to handle the supertypes cases and the ALL of the subtypes. Given this example:
public type SubType = {
    #one;
    #two;
};
public type SuperType = {
  #three;
};

switch (superType) {
  case (#three) processThree();
  case (#two) processSubType(#two); 
  case (#one) processSubType(#one);
};

public func processSubType(subType : SubType) {
    ...
};

I want it to be less redundant like:

switch (superType) {
  case (#three) processThree();
  // Remaining cases have to be the subtype
  case (subType) processSubType(subType); 
};

(Variant subset matching)

  1. Problem - Clunky string concat and value stringification(?)
    Its just annoying to write out strings with # and no interpolation
let value = "This is some " # someToTextFunc(v) # " text that im writing and took me " # Nat.toText(x) # " seconds to come up with";

Solution: ?
I don’t have a good solution to making stringification better because of structural typing makes it hard to know how to format the value. An option is to make anything that is not Text default to whatever debug_show does, but I feel like that might be dangerous and make it easy to make mistakes or not understand how its formatting them. But at least with interpolation that seems more straight forward

let value = $"This is some {someToTextFunc(v)} text that im writing and took me {Nat.toText(x)} seconds to come up with"
  1. Problem - Inline functions aren’t quite there/Type inference problems
    A lot of the time I just want to do something like a simple map like:
let v : [Nat] = [1, 2, 3];
Array.map(v, func(x) = x + 1);

But the code above doesn’t work because cannot infer type of variable is an error. So either the param types/return types have to be defined in the function or on the Array.map. This becomes more and more of a problem with longer type names and more parameters. It seems like it should have enough information to infer, but it isnt able to.
Also it helped that i found the func(...) = ... syntax vs a normal func but its not quite there.

14 Likes
  1. Problem - Library code organization without exposing APIs
    Maybe there is a way of doing this already but the ideal is that I want to organize my code into separate files but have that code be ‘internal’ only so that its only exposed to the other files. This way if someone uses my library, they can’t accidentally use an internal function that may change. Not sure how this would work because the ‘library’ really is just a collection of files someone adds their to their code base, but it feels weird to have everything in one file and private or public and not documented.
1 Like
  1. Lack of binary literals
    Nice to have but 0b10101010 would be super helpful instead of just hex literals
4 Likes
  1. Enumerable state machines
    This is inspired by C#, and a bit of a complex nice to have, but since there is a lot of focus on Iter types, it would be nice to have code sugar to ‘continue’ the code from the last return to iterate through items
public class MyIter() : Iter.Iter<Nat> {
  public func next() : ?Nat {
    while (...) {
       yield return x;
       yield return y;
       ....
    }
  };
};

Where a yield return or something similar would ‘pause’ evaluation and continue on the next call

1 Like
  1. Can this be accomplished with a combo of let/else and do option blocks?
    let-else
    option-blocks

  2. Not syntax sugar, but close: Function compose

  3. Definitely something could be done as far as syntactic sugar for representing variants would be helpful (although you can use wildcard _ to do something similar).

  4. I agree.

  5. I agree, less of a problem imho (just part of typing safety).

  6. You can use an outer public (file scope) and inner private module, then use a record literal at the end of the public module (after the inner private module), whose fields are the functions the inner private module. Then the functionality of that inner private module is exposed without it’s being altered.

  7. Agree.

  8. Seems like variants could play a part in accomplishing this.

2 Likes
  1. i forgot about let else, I’ll have to try it, but i still think native Result<> would be cool

  2. seems limited with having to nest compose but another one i haven’t tried. Ty

  3. didn’t know about nested modules. Might still have large files but could be helpful

Perhaps related to 1. I’d like some syntactic sugar to unwrap deeply nested optional values and easily log where evaluation stopped in case there is a null element along the way. Right now only the former is possible using do ? blocks afaik.

Rust has ok_or method for Option types that seem to do just that:

let host = ingress
    .status.ok_or(MyError::new("Ingress status is missing"))?
    .load_balancer.ok_or(MyError::new("Load balancer status is missing"))?
    .ingress.ok_or(MyError::new("Load balancer ingress is missing"))?
    .first().ok_or(MyError::new("No load balancer ingress found"))?
    .hostname.ok_or(MyError::new("Hostname is missing"))?;
2 Likes

Totally forgot

  1. Problem - Cant deserialize to a custom data structure/Type reflection

Ive written a couple serialization libraries for candid, cbor and now xml but all I can seem to do is return a cbor, candid or xml object but people always ask to have auto conversion to their custom types like:

public type MyType = {
  one : Text;
  two : Nat;
};
let value : MyType = deserialize<MyType>(candidCborOrXml);

Probably one of the harder ones but the ability to inspect the type details and do this would be huge

1 Like

#5 is a big inconvenience for me

1 Like

Couple of comments:

  1. [Error propagation]. I agree. Generalising the do? construct to arbitrary types has been on the roadmap for a while.

  2. [Pipeline operator] While nested calls can get ugly occasionally, I don’t think they justify special syntax, and most languages get along fine without it. A pipeline operator is really only useful in conjunction with currying (or dubious syntax hacks).

  3. [Collecting variant cases] Not entirely obvious, and very hard in general, but lately I’ve been thinking that there may be some reasonably clean and simple way of supporting this.

  4. [String interpolation] I’m always a bit puzzled by this. If you consider "# and #" instead of { and } your interpolation delimiters, then you already have it: "abc "#f(x)#" def". The suggested $"abc {f(x)} def" saves exactly one character over that.

  5. [Stronger type inference] Understandable, but would require a fundamentally different and more complicated type system (general type inference with subtyping instead of bidirectional typing). I’m not sure it’s worth it, and has the tendency to lead to much more obscure type error messages.

  6. [Private modules in packages] Agreed. My recollection was that vessel actually was supposed to support exposing only a selected set of modules from a package, but it’s been a while. Other than that, you can use nested modules, but then it’s all in one file, which usually is not what you want.

  7. [Binary literals] Yes, that would be an easy addition.

  8. [Generators] That’s a bit difficult to compile to Wasm right now, but proposals are being developed to make it easier, by providing forms of stack switching. However, the larger problem with the specific way you suggest it is that that would introduce a form of hidden global state, which I’m very doubtful about, and which would be extremely difficult to reconcile with canister upgrades. So if anything, I suspect we’d rather have more conventional generators that produce an iterator.

  9. [Deserialisation] Not sure I understand the example. Doesn’t from_candid already allow you to deserialise from Candid into MyType? Other than that, you’d need a form of typeswitch, which is a highly complicated (and questionable) feature in a rich type system. Don’t hold your breath. For many practical use cases, something similar can be achieved by a combinator library.

5 Likes

from_candid does work, it’s more for the other ones, or if you want to add more customization. Doesn’t seem like an easy issue :stop_sign::lungs:

1 Like

Not sure if it still works that way, but a while ago I made it so package imports cannot “leave” the package root, so you can’t say import thing "mo:matchers/../../File"

From within your package those are just relative paths, so if you structure your package like so:

src/
  ExposedModule.mo
internal/
  InternalModule.mo

people using your library can only reference ExposedModule, but you can import import "../internal/InternalModule" from your own library. And because that logic sits within the compiler you don’t need to build it into vessel, or another package manager.

6 Likes

Very cool. I’ll try this out. Ty

1 Like

It would be nice to have the “with” syntax work for types:

let service_actor : {
          service.a_service with 
          get_couner: query () -> async nat;} = await servicea_service(null);
1 Like

@skilesare:

let service_actor : service.a_service and {get_counter : query () -> async nat} =
   await servicea_service(null);

Unfortunately, the expression syntax is asymmetric to that…

I was thinking about this some more.
Is it possible to do something like Rust Macros because that would probably solve the issue of serialization and add a lot more complex functionality while being type safe.
It doesn’t seem like a trivial thing but would be game changing

While we’re all here, and given the meeting later today, I’ve been loving the

let ?x = something() else return "X";

…but the second item is a None() and thus requires a return…unless I’m using it incorrectly.

it would be great to have one that works with the same type so we could do:

 let ?x = something() defaults 9 : Nat;

I know that is the same as let x = Option.get(something(), 9:Nat), but from a language-feeling standpoint it is a lot neater and we don’t have to teach people about the Option library before we absolutely have to.

2 Likes

It would be great to have the ability to index tuples with variables.
I think compiler could identify let declarations that are statically known and allow such declarations to be used to index tuples.

So instead of doing this

let tupleProperty = tuple.0;

we could do this

let PROPERTY_NAME = 0;

let tupleProperty = tuple[PROPERTY_NAME];

It would greatly help with readability for code that uses tuples frequently
I miss the ability to have humanly readable property names for tuples

1 Like

Dont know if this helps your case but you can deconstruct a tuple like this

let (a: A, b: B) = tuple;

That would mean a dependently-typed accessor (indexing) function, because the tuple components can be of potentially different types.