Skip to content

YS Currying in YAML Configs

In functional programming, currying is a technique where a function that takes multiple arguments is called with fewer arguments than it takes and returns a function that takes the remaining arguments.

In Clojure and YS, the partial function is used to create a curried function.

Here's an example:

$ ys -e '
multiply-by-6 =: partial(mul 6)
multiply-by-7 =: mul.partial(7)
say: multiply-by-6(7)
say: multiply-by-7(6) 
'
42
42

That's neat but how is it useful in YAML configs?

Curried merge🔗

Imagine you have a big YAML config with a lot of mappings that often have the same data or almost the same data.

It would be nice if there was a simple way to use defaults, and just specify the overrides.

Here's part of an example YAML config I just found on the internet:

# From https://circleci.com/docs/sample-config/
jobs:
  build:
    docker:
    - image: cimg/base:2023.03
    steps:
    - checkout
    - run: echo "this is the build job"
  test:
    docker:
    - image: cimg/base:2023.03
    steps:
    - checkout
    - run: echo "this is the test job"

Let's refactor this with YS:

!YS-v0

defaults =::
  docker:
    docker:
    - image: cimg/base:2023.03

--- !data
jobs:
  build::
    merge defaults.docker::
      steps:
      - checkout
      - run: echo "this is the build job"
  test::
    merge defaults.docker::
      steps:
      - checkout
      - run: echo "this is the test job"

Let's load this with ys:

$ ys -Y config.yaml
jobs:
  build:
    docker:
    - image: cimg/base:2023.03
    steps:
    - checkout
    - run: echo "this is the build job"
  test:
    docker:
    - image: cimg/base:2023.03
    steps:
    - checkout
    - run: echo "this is the test job"

It works, but it's not very nice looking...

Yet!

Let's put the defaults in a separate file, and curry the defaults into the merge function.

# defaults.yaml
docker:
  docker:
  - image: cimg/base:2023.03
!YS-v0:
defaults =: load('defaults.yaml')
docker-defaults =: merge.partial(defaults.docker)

jobs:
  build: !:docker-defaults
    steps:
    - checkout
    - run: echo "this is the build job"
  test: !:docker-defaults
    steps:
    - checkout
    - run: echo "this is the test job"

And load:

$ ys -Y config.yaml
jobs:
  build:
    docker:
    - image: cimg/base:2023.03
    steps:
    - checkout
    - run: echo "this is the build job"
  test:
    docker:
    - image: cimg/base:2023.03
    steps:
    - checkout
    - run: echo "this is the test job"

It works, and it's a lot cleaner this time.

We used tag function calls to our curried merge function.

If this was a real world config, it would have been much bigger. We could have put lots of defaults in the defaults.yaml file. Then we could make functions to apply the defaults wherever we wanted to.

I hope you can see how powerful a refactoring strategy like this can be!

Comments