Gradual typing in Dhall - rowtype

The Dhall website has a nice example:
{- You can optionally add types
`x : T` means that `x` has type `T`
-}
let Config : Type =
{- What happens if you add another field here? -}
{ home : Text
, privateKey : Text
, publicKey : Text
}
let makeUser : Text -> Config = \(user : Text) ->
let home : Text = "/home/${user}"
let privateKey : Text = "${home}/.ssh/id_ed25519"
let publicKey : Text = "${privateKey}.pub"
let config : Config = { home, privateKey, publicKey }
in config
let configs : List Config =
[ makeUser "bill"
, makeUser "jane"
]
in configs
Like the comment asks me to do, I add another field: foo : Text to the type Config and now the example fails to typecheck.
This leaves me without a way to gradually add types to existing projects. I'm missing an escape hatch to allow me to gradually add knowledge as I become more familiar with the dynamic configuration of a project that I don't own.
I bet this is a common situation: "I'm maintaining a project I don't own, configured in YAML or JSON without a schema, or with a loosely defined schema."
I believe what I want is called "row-polymorphism" which seems to have already been discussed (and rejected?) in the Dhall issues, but what alternative do I have to solve the same issue? From my perspective, it seems like I cannot use Dhall in the case where I don't have total information?

You're right that the most ergonomic solution would be language support for row polymorphism. See, for example, another proof-of-concept project of mine which does support row polymorphism:
Fall-from-grace
However, Dhall does have a language feature which can still help here, which is support for projecting a record's fields by the expected type. In other words, given a record r containing a superset of the fields in Config, you can project out just the fields of interest using r.(Config).
Normally I'd illustrate how this would work in the context of the example you referenced from dhall-lang.org, but the issue for that specific example is that there's no way it can silently ignore the presence of the extra foo field, even if Dhall were to support row polymorphism. This is because the makeUser function produces a Config instead of consuming a Config, so if you commit to producing that extra field then you have to actually add the expected field.
However, in the spirit of your request I'll use a related example which I believe is closer to what you had in mind. The only difference is that we'll change our function to one that consumes a Config:
let Config : Type = { home : Text, privateKey : Text, publicKey : Text }
let render =
λ(config : Config) →
''
Home: ${config.home}
Private Key: ${config.privateKey}
Public Key: ${config.publicKey}
''
let example =
{ home = "/home/gabriella"
, privateKey = "/home/gabriella/.ssh/id_ed25519"
, publicKey = "/home/gabriella/.ssh/id_ed25519.pub"
}
in render example
We'd like to be able to add another foo field to example but without breaking the render function. In other words, we want the render function to silently ignore any fields other than the ones in the Config type.
Normally, in a language with row polymorphism you'd address this by changing the type of render to something like this:
let render =
λ(config : { home : Text, privateKey : Text, privateKey : Text | r }) →
''
Home: ${config.home}
Private Key: ${config.privateKey}
Public Key: ${config.publicKey}
''
let example =
{ home = "/home/gabriella"
, privateKey = "/home/gabriella/.ssh/id_ed25519"
, publicKey = "/home/gabriella/.ssh/id_ed25519.pub"
, foo = "bar"
}
in render example
… where the r in the type of render denotes "other fields that we want to ignore". Dhall doesn't support that, as you noted, but if we use Dhall's support for projecting fields by type, we can do this:
let Config : Type = { home : Text, privateKey : Text, publicKey : Text }
let render =
λ(config : Config) →
''
Home: ${config.home}
Private Key: ${config.privateKey}
Public Key: ${config.publicKey}
''
let example =
{ home = "/home/gabriella"
, privateKey = "/home/gabriella/.ssh/id_ed25519"
, publicKey = "/home/gabriella/.ssh/id_ed25519.pub"
, foo = "bar"
}
in render example.(Config)
It is not as ergonomic as row-polymorphism, but at least it addresses the use case of making the code insensitive to changes in the upstream input that add new fields.

Related

How to access a local using a variable in Terraform

I have the following code.
mymodule
variable "senses" {
type = string
}
locals {
sounds = {
"cat" = "meow"
"dog" = ["bark", "woof"]
}
}
output "noise" {
value = local[var.senses]["cat"]
}
call mymodule
module "mymodule" {
source = "../../../modules/mymodule"
senses = "sound"
}
returns error:
Error: Invalid reference
on ../../../modules/mymodule/outputs.tf line 62, in output "noise":
62: value = local[var.senses]["cat"]
The "local" object cannot be accessed directly. Instead, access one of its
attributes.
my code can't seem to handle
value = local[var.senses]["cat"]
Any suggestions on how i can get this to work?
I don't believe it's possible to use a variable to switch which local you're reading. I.e. local[var.senses] is the root of the issue.
If you refactor slightly and put your values inside a single, known, value--such as local.senses it should then let you do a key lookup within that value.
So, if you modify your locals to place your values in a senses key:
locals {
senses = {
"sounds" = {
"cat" = "meow"
"dog" = ["bark", "woof"]
}
}
}
and update your lookup to use that field:
value = local.senses[var.senses]["cat"]
Then I believe it will work, since your are doing a key lookup against a specific local rather than trying to dynamically select the local.

get title from Zotero item

Still very new to Rust, trying to understand how to extract the title of a JournalArticle using the Zotero crate.
I've got this, and can confirm the item is retrieved successfully:
let zc = ZoteroCredentials::new();
let z = ZoteroInit::set_user(&zc.api_id, &zc.api_key);
let item = z.get_item(item_id, None).unwrap();
From here, I see that an item.data is an ItemType, specifically a JournalArticleData. But I'm fundamentally not quite understanding how to either a) serialize this to JSON, or b) access .title as a property.
For context, this would be the result of a Rocket GET route.
Any help would much appreciated!
It sounds like the part you're missing is how to use pattern matching on an enum. I'm not familiar with zotero so this is all based on the docs, with verbose type annotations to be explicit about what I think I'm doing:
use zotero::data_structure::item::{Item, ItemType, JournalArticleData};
let item: Item = z.get_item(item_id, None).unwrap();
// Now we must extract the JournalArticle from the ItemType, which is an enum
// and therefore requires pattern matching.
let article: JournalArticleData = match item.data {
ItemType::JournalArticle(a) => a,
something_else => todo!("handle wrong type of item"),
}
let title: String = article.title;
(The match could also be written as an if let just as well.)
You could also use pattern matching to go through the entire structure, rather than only the enum which requires it:
match z.get_item(item_id, None).unwrap() {
Item {
data: ItemType::JournalArticle(JournalArticleData {
title,
..
}),
..
} => {
// Use the `title` variable here
},
something_else => todo!("handle wrong type of item"),
}

Terraform: How to Deal with Optional Input Variable

So I came across this general problem and didn't find an answer yet.
Problem: The input value can have optional variables, like the case below, group_memberships is an optional input, at the moment I make it an empty string input for this to work.
But if I comment it out like shown below and run it, I would get the error:
The given key does not identify an element in this collection value.
Basically it's complaining that I don't have list_of_users.test_user.group_memberships.Is there a way to tell terraform if the input is not declared, just ignore it? I know I can leave it the way it is but user can potentially have many optional values, and making lots of empty input doesn't really make sense.
Thanks! First post question, sorry about poor layout for the code : )
in my .tfvars file:
list_of_users = {
regular_user = {
email = "pdv#abc.com",
group_memberships = "regular_group"
},
test_user = {
email = "test#abc.com",
// group_memberships = "" <------ Currently can work if not comment out, looking for solution that I can remove those reduent empty declariation
},
admin_user = {
email = "admin#abc.com",
group_memberships = "admin_group"
}
}
in .tf file:
variable "list_of_users" {}
resource "user_api_from_provider" "user_generate" {
for_each = var.list_of_users
email = each.value["email"]
group_memberships = each.value["group_memberships"] !=""? [user_api_from_provider.group_generate[each.value["group_memberships"]].id] : null
}
There is support for this as a Terraform "experiment" (it's implemented, but could change or be removed in future versions). You have to declare in your module that you're using the experiment:
terraform {
# Optional attributes and the defaults function are
# both experimental, so we must opt in to the experiment.
experiments = [module_variable_optional_attrs]
}
And then you would use it in your case like this:
variable "list_of_users" {
type = map(object({
email = string
group_memberships = optional(string)
}))
}
Now, if group_membership isn't defined for a given user, that field will have the value of null, so you can now do:
resource "user_api_from_provider" "user_generate" {
...
group_memberships = each.value.group_memberships != null ? [user_api_from_provider.group_generate[each.value["group_memberships"]].id] : null
}
Alternatively, if you don't want to use the experiment, you should be able to do this (untested):
resource "user_api_from_provider" "user_generate" {
...
group_memberships = contains(each.value, "group_memberships") ? [user_api_from_provider.group_generate[each.value["group_memberships"]].id] : null
}
As of Terraform v1.3 the Optional Object Type Attributes feature is official, which means it is no longer an experiment and the syntax is considered stable.
As mentioned in previous comments, you can now do something like:
variable "list_of_users" {
type = map(object({
email = string
group_memberships = optional(string, "")
}))
}
In the above example, using the default value ("") allows the Terraform code in the project/module to function as if there is always a value even if it is omitted from the input variables.

Delayed evaluation / references to atomic datatypes in data structures

In Python 3, I use serialize data to JSON like so:
import json
config = {
'server' : {
'host' : '127.0.0.1',
'port' : 12345
}
}
serialized_config = json.dumps(config)
Due to json.dumps and the fact that the format of the JSON is fixed, I cannot alter the struct
of the python data arbitralily, e.g. host needs to be a string when config is passed to json.dumps. I cannot wrap it in a list like 'host' : ['1.1.1.1]' e.g.
Now the data is way more complex than in this example, but mostly redundant (e.g. there is only one 'host' that is constant but appears in various places throughout the config.
Now I like to define some template like so:
template = {
'server' : {
'host' : SERVER_IP,
'port' : 12345
}
}
And afterwards replace all occurences of e.g. SERVER_IP with the actual server ip etc.
The point is that I don't know the value of SERVER_IP when I define the template.
The easy way would be to just shift the atomic string into a non-atomic python struct, e.g.:
server = {
'host' : '1.1.1.1',
'port' : 12345
}
template = {
'server' : server
}
...
# Here the actual value of 'host' has been set
server['host'] = '192.168.0.1'
print(json.dumps(template))
This dumps
'{"server": {"host": "192.168.0.1", "port": 12345}}'
As wanted.
However, since port etc. also change, I would prefer something like (pseudo-code):
server_ip = '1.1.1.1' # not the actual value
template = {
'server' : {
'host' : ref(server_ip), # Don't copy the string, but either delay the evaluation or somehow create a ref to the string
'port' : 12345
}
}
...
server_ip = '192.168.0.1'
I am aware that I could write code that just traverses the python data and 'manually' replaces certain strings with different values.
However, I am curious if there is some better solution.
E.g. I played around with facilitating __getattribute__ like so:
class DelayedEval:
def __init__(self, d):
self.data = d
def __getattribute__(self, key):
# Recursive matters let aside for sake of simplicity...
data = object.__getattribute__(self, 'data')
v = data[key]
try:
v = v()
except:
pass
return v
# possibly overwrite __getitem__() etc as well ...
server_ip = '1.1.1.1'
template = {
'server' : {
'host' : lambda: server_ip, # prevents 'premature evalutation'
'port' : 12345
}
}
...
server_ip = '192.168.0.1'
print(json.dumps(DelayedEval(template))) # Does not work
I am not asking for fixing this code, I am asking for any solution that solves my issue, no matter how, as long as You deem it 'better' than the bute-force approach of traverse and replace manually.
Fixing this code might solve my problem, but there might be better solutions (or none?).
I am aware that this is a rather complicated question to pose, and probably for my usecase overly complex, however, I am curious whether there would be a viable solution to this problem apart from the brute-force 'search-and-replace' approach...
Is there some way to define 'self-completing template structs' like that in Python?
One easy approach is to make the template itself a lambda function:
template = lambda: {
'server' : {
'host' : server_ip,
'port' : 12345
}
}
...
server_ip = '192.168.0.1'
print(json.dumps(template()))

Terraform module with repeatable variable

Several resources, e.g. aws_dynamodb_table have repeatable variables. In the case of the aws_dynamodb_table resource, attribute is repeatable which allows you to specify multiple attributes using either of the following syntax
attribute {
name = "UserId"
type = "S"
}
attribute {
name = "GameTitle"
type = "S"
}
attribute {
name = "TopScore"
type = "N"
}
or
attribute = [{
name = "UserId"
type = "S"
}, {
name = "GameTitle"
type = "S"
}, {
name = "TopScore"
type = "N"
}]
I like this interface and want to provide the same flexibility in my modules but I can't seem to find any documentation on how to do it. Is this possible for modules or is it only the built-in resources that can do this.
It looks like that either allows you to provide attribute multiple times as separate maps (which are then merged) or as a list.
You're going to want to take a look at the documentation related to Input Variable Configuration
In particular, you will want to look at the section titled Variable Merging.
I believe you could do something like this for similar behavior (from the docs above, give them a read :P)
foo {
quux="bar"
}
foo {
bar="baz"
}
This would mean foo returns:
{
quux = "bar"
bar = "baz"
}
Hope that helps!

Resources