With the Cloud it's possible to host arbitrary typed Unison functions as services. Calls to Unison native services take place over the network without the need for HTTP requests, JSON blobs, or other serialization formats. We'll write a simple native service here to introduce the Services API.

πŸ“š What we'll cover

  • Deploying a Unison native service to the Cloud
  • Using the Services ability for calling remote functions
  • The API for running Cloud jobs vs. deploying Cloud services

Heads up! This exercise has three parts to it so don't worry if your first submission doesn't pass all the tests in the print out. We'll guide you through the subsequent steps. 😊

If you haven't already created a local workspace for the exercises in the cloud-start project, get the stubs and exercise runner with the following commands:

.> project.create-empty cloud-start
cloud-start/main> pull @unison/cloud-start/releases/latest

πŸŸͺ Part 1: Deploy a simple native service

Implement a Unison Cloud Native service that expects a Nat as its argument and returns a Nat that is the square of the input. (Again, we're just focusing on the mechanics of deploying and calling a service here. 😊)

πŸ“ Instructions

To get started run the following command in the UCM:

cloud-start/main> edit exercises.ex2_nativeService.deploy

It should look something like this to start:

exercises.ex2_nativeService.deploy : '{IO, Exception} ServiceHash Nat Nat
exercises.ex2_nativeService.deploy =
  todo "Implement the Cloud deployment and service logic. The deployed service should square a Nat value"

Unison native services are deployed to the Cloud using the Cloud.deploy function. Cloud.deploy takes two arguments, an Environment where the service should be deployed and a function to be run as a service.

Cloud.deploy : Environment
               -> (a ->{Config, [...], Scratch} b)
               -> {Exception, Cloud} ServiceHash a b

A native service is fundamentally a function from a to b which runs on the Cloud instead of on your local machine.

The second argument is where you should put the logic which squares (via Nat.pow) a given number.

πŸ“ When you're ready to submit your solution, save the file and update your codebase before running the following:

cloud-start/main> run submit.ex2_nativeService

The general pattern for these service exercises will involve writing service "business logic" functionality, creating the cloud resources that your service needs to run, and then deploying it. We'll use the ServiceHash returned by Cloud.deploy to call your service in tests.

πŸ”₯ Part 2: Practice returning errors

Take a closer look at the service logic argument from Cloud.deploy, it looks a good deal more complex than a -> b because there's actually a set of abilities included in the function signature.

(a -> {Config,
    Scratch} b)

These abilities are available to your service when it runs on the Cloud. For example, your services might write Log messages on errors, or issue HTTP requests with Http.

πŸ“ Instructions

Update the implementation of the service to raise an error when trying to square a number that would cause a Nat overflow.

cloud-start/main> edit exercises.ex2_nativeService.deploy

Your service should raise an Exception with a message containing the string "cannot be squared".

Check out ex2_nativeService.failure for a pre-made Failure type to use.

exercises.ex2_nativeService.failure : Failure
exercises.ex2_nativeService.failure =
  Failure (typeLink Nat) "cannot be squared" (Any ())

When you're ready, save the file, update again, and run the following:

cloud-start/main> run submit.ex2_nativeService

πŸ“ž Part 3: How do you call a native service?

Let's say you need to call the service you just deployed in another Unison Cloud job. You can't use an HTTP client for a native service, but you can use the Services ability for calling typed remote functions.

The Services ability exposes two primary functions for calling remote services: Services.call and Services.callName.

Services.call : ServiceHash a b -> a ->{Services, Remote} b
Services.callName : ServiceName a b -> a ->{Services, Remote} b

Service hashes identify a single service deployment while a service name refers to the latest of potentially many service deployments over time. Our deployment function just returns a hash, so we'll use Services.call with the test value 5 as an argument.

Next we need to deploy this service call to the Cloud to evaluate it. We've seen Cloud.deployHttp and Cloud.deploy used to lift a function to the Cloud as a service, with inputs and outputs, but jobs like this one, which aren't expressed in terms of dynamic inputs and outputs, can be run on the Cloud with Cloud.submit:

Cloud.submit : Environment
               -> '{Services, [...], Scratch} a
               -> {Exception, Cloud} a

The second argument to Cloud.submit is a delayed computation that returns a single value.

πŸ“ Instructions

Call ex2_nativeService.deploy with the argument 5 in a new Cloud job function, ex2_nativeService.callService.

cloud-start/main> edit exercises.ex2_nativeService.callService

When you're ready to submit your solution, save the file, update your codebase, and run the following command:

cloud-start/main> run submit.ex2_nativeService