Skip to content

Instantly share code, notes, and snippets.

@skeeto
Last active August 29, 2021 22:22
Show Gist options
  • Save skeeto/d8b18fb4663f4a282e8c0db252a70faf to your computer and use it in GitHub Desktop.
Save skeeto/d8b18fb4663f4a282e8c0db252a70faf to your computer and use it in GitHub Desktop.
quniq
@skeeto
Copy link
Author

skeeto commented May 5, 2020

Could you please give me a good GitHub example that is not too complex but implements a configuration struct

https://github.com/skeeto/passphrase2pgp/blob/870b0a4/passphrase2pgp.go#L83

That struct is filled out by the option parser and gets passed around the program so that the different components can access the parts of the configuration they need. If I add or remove an option I don't have to change all the call sites to add/remove parameters/arguments.

and a "filter" interface?

Here are two approaches: functional and interface. I'll start with the one I prefer:

package main

import (
	"fmt"
	"strings"
)

type filter func(string) string

func identity(s string) string {
	return s
}

func chain(a, b filter) filter {
	return func(s string) string {
		return b(a(s))
	}
}

func substring(beg, end int) filter {
	return func(s string) string {
		return s[beg:end]
	}
}

func reverse(s string) string {
	r := []rune(s)
	for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
		r[i], r[j] = r[j], r[i]
	}
	return string(r)
}

func main() {
	f := filter(identity)
	f = chain(f, strings.ToLower)
	f = chain(f, substring(0, 4))
	f = chain(f, reverse)
	fmt.Println(f("FooBar"))
}

Note how I start with the identity filter, then chain on new filters dynamically. Your option parser could append new filters as it parses arguments, so that the order of filters depends on the order the of arguments. Also notice how I could use strings.ToLower directly because it was already a filter.

Here's the interface version:

package main

import (
	"fmt"
	"strings"
)

type filter interface {
	Transform(string) string
}

type identity struct{}

func (_ *identity) Transform(s string) string {
	return s
}

type chain [2]filter

func (c *chain) Transform(s string) string {
	return c[1].Transform(c[0].Transform(s))
}

type lower struct{}

func (_ *lower) Transform(s string) string {
	return strings.ToLower(s)
}

type substring struct{ beg, end int }

func (x *substring) Transform(s string) string {
	return s[x.beg:x.end]
}

type reverse struct{}

func (_ *reverse) Transform(s string) string {
	r := []rune(s)
	for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
		r[i], r[j] = r[j], r[i]
	}
	return string(r)
}

func main() {
	f := filter(&identity{})
	f = &chain{f, &lower{}}
	f = &chain{f, &substring{0, 4}}
	f = &chain{f, &reverse{}}
	fmt.Println(f.Transform("FooBar"))
}

Quite a bit more verbose, but potentially more flexible if you filters become more complex. In the function version the substring filter captured its configuration in a closure. In the interface version it's stored on a struct.

@jftuga
Copy link

jftuga commented May 6, 2020

Thanks again for another great reply - and what a cool reverse() function! I also noticed that your passphrase code imports your own optparse library. The configuration struct idea seems pretty straight forward.

The functional version it easier for me to follow. The chaining concept looks really useful. I am going to experiment with it. OTOH, the interface version seems syntactically cumbersome and is harder for me to follow. I think ease of use is going to win out for me vs potential flexibility.

@skeeto
Copy link
Author

skeeto commented May 6, 2020 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment