You can host arbitrary typed Unison functions as services on the Cloud. 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

We'll be implementing a Unison Cloud Native service that expects a Nat as its argument and returns a Nat that is the square of the input. (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

A native service is a function, a -> b, which runs on the Cloud instead of on your local machine.

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 ->{Environment.Config, [...], Scratch} b)
               -> {Exception, Cloud} ServiceHash a b

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

In your scratch file, change the ex2_nativeService.deploy function to call Cloud.deploy with the exercises.env() Environment and a function that squares a Nat value.

πŸ“ 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
Solution
exercises.ex2_nativeService.deploy : '{IO, Exception} ServiceHash Nat Nat
exercises.ex2_nativeService.deploy = Cloud.main do
  Cloud.deploy exercises.env() (n -> Nat.pow n 2)

The general pattern for these 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 -> {Environment.Config,
    Exception,
    Storage,
    Http,
    Services,
    Remote,
    Random,
    Log,
    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 Exception when trying to square a number that would cause a Nat overflow.

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

The argument to Exception.raise is a Failure type that contains some type information and a message. For convenience, your service should raise the ex2_nativeService.failure value when the input is too large.

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
Solution
exercises.ex2_nativeService.deploy : '{IO, Exception} ServiceHash Nat Nat
exercises.ex2_nativeService.deploy = Cloud.main do
  threshold = sqrt (Float.fromNat maxNat) |> Float.ceiling |> Float.toNat
  Cloud.deploy exercises.env() (n ->
    (if Optional.exists (t -> n >= t) threshold then
      Exception.raise ex2_nativeService.failure
    else Nat.pow n 2)
  )

πŸ“ž 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.

πŸ“ Instructions

We'll practice calling the service you just deployed in a new Cloud job function, ex2_nativeService.callService.

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

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.

The deployment function we just wrote returns a hash, so we can use it directly as an argument to Services.call.

Services.call ex2_nativeService.deploy() 5

Next we need to run this service call on 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 your native service with the argument 5 in 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
Solution
exercises.ex2_nativeService.callService :
    '{IO, Exception} Nat
exercises.ex2_nativeService.callService =
    Cloud.main do
      serviceHash = exercises.ex2_nativeService.deploy()
      Cloud.submit exercises.env() do
        Services.call serviceHash 5