7
\$\begingroup\$

I am learning Go and as an exercise I did this challenge from Code Golf (without the golfing), where an ASCII art snowman is drawn from a combination of body parts, based on an 8-digit code taken as command line argument.

For example:

$ ./snowman 12431312

 _===_ 
 (-.O) 
<( : )\
 ( : ) 

As a minor extension I decided to use a random code if none is passed.

Coming from Python, I had hoped to use the static typing to my advantage (e. g. by checking at compile time that the definition of body parts is valid), but I have the feeling that I am fighting it more than it helps me.

I'd like to know how I can make the code less verbose and repetitive, and how I can use the static typing to make the code less error-prone and brittle.

snowman.go

package main

import (
    "errors"
    "fmt"
    "math/rand"
    "os"
    "strconv"
    "time"
)

type Snowman [5][7]rune

func (s Snowman) getLine(i int) string {
    var line string
    for _, c := range s[i] {
        line += string(c)
    }
    return line
}

func (s Snowman) String() string {
    var result string
    for i := range s {
        if i > 0 {
            result += "\n"
        }
        result += s.getLine(i)
    }
    return result
}

type Hat [2][5]rune
type Nose rune
type Eye rune
type LeftArm [2]rune
type RightArm [2]rune
type Torso [3]rune
type Base [3]rune

var hats = [...]Hat{
    {{' ', ' ', ' ', ' ', ' '}, {'_', '=', '=', '=', '_'}},
    {{' ', '_', '_', '_', ' '}, {'.', '.', '.', '.', '.'}},
    {{' ', ' ', '_', ' ', ' '}, {' ', '/', '_', '\\', ' '}},
    {{' ', '_', '_', '_', ' '}, {'(', '_', '*', '_', ')'}},
}

var noses = [...]Nose{',', '.', '_', ' '}

var eyes = [...]Eye{'.', 'o', 'O', '-'}

var leftArms = [...]LeftArm{{' ', '<'}, {'\\', ' '}, {' ', '/'}, {' ', ' '}}

var rightArms = [...]RightArm{{' ', '>'}, {'/', ' '}, {' ', '\\'}, {' ', ' '}}

var torsos = [...]Torso{
    {' ', ':', ' '},
    {']', ' ', '['},
    {'>', ' ', '<'},
    {' ', ' ', ' '},
}

var bases = [...]Base{
    {' ', ':', ' '},
    {'"', ' ', '"'},
    {'_', '_', '_'},
    {' ', ' ', ' '},
}

// newSnowman returns a Snowman with no hat, arms, face, torso, or base.
func newSnowman() Snowman {
    var s Snowman
    for _, i := range [3]int{0, 1, 4} {
        s[i][0] = ' '
        s[i][6] = ' '
    }
    for i := 2; i < 5; i++ {
        s[i][1] = '('
        s[i][5] = ')'
    }
    return s
}

func (s *Snowman) setHat(h Hat) {
    for i, line := range h {
        for j, c := range line {
            s[i][j+1] = c
        }
    }
}

func (s *Snowman) setNose(n Nose) {
    s[2][3] = rune(n)
}

func (s *Snowman) setLeftEye(e Eye) {
    s[2][2] = rune(e)
}

func (s *Snowman) setRightEye(e Eye) {
    s[2][4] = rune(e)
}

func (s *Snowman) setLeftArm(a LeftArm) {
    for i, c := range a {
        s[i+2][0] = c
    }
}

func (s *Snowman) setRightArm(a RightArm) {
    for i, c := range a {
        s[i+2][6] = c
    }
}

func (s *Snowman) setTorso(t Torso) {
    for i, c := range t {
        s[3][i+2] = c
    }
}

func (s *Snowman) setBase(b Base) {
    for i, c := range b {
        s[4][i+2] = c
    }
}

type SnowmanCode [8]int

func snowmanCodeFromString(s string) (SnowmanCode, error) {
    var result SnowmanCode
    if len(s) != 8 {
        return result, errors.New("expected 8 digits")
    }
    for i, digit := range s {
        num, err := strconv.Atoi(string(digit))
        if err != nil {
            return result, err
        }
        result[i] = num
    }
    return result, nil
}

func randomCode() SnowmanCode {
    return SnowmanCode{
        rand.Intn(len(hats)) + 1,
        rand.Intn(len(noses)) + 1,
        rand.Intn(len(eyes)) + 1,
        rand.Intn(len(eyes)) + 1,
        rand.Intn(len(leftArms)) + 1,
        rand.Intn(len(rightArms)) + 1,
        rand.Intn(len(torsos)) + 1,
        rand.Intn(len(bases)) + 1,
    }
}

func SnowmanFromCode(c SnowmanCode) (Snowman, error) {
    s := newSnowman()
    if !(1 <= c[0] && c[0] <= len(hats)) {
        return s, errors.New("hat code out of range")
    }
    if !(1 <= c[1] && c[1] <= len(noses)) {
        return s, errors.New("nose code out of range")
    }
    if !(1 <= c[2] && c[2] <= len(eyes)) {
        return s, errors.New("left eye code out of range")
    }
    if !(1 <= c[3] && c[3] <= len(eyes)) {
        return s, errors.New("right eye code out of range")
    }
    if !(1 <= c[4] && c[4] <= len(leftArms)) {
        return s, errors.New("left arm code out of range")
    }
    if !(1 <= c[5] && c[5] <= len(rightArms)) {
        return s, errors.New("right arm code out of range")
    }
    if !(1 <= c[6] && c[6] <= len(torsos)) {
        return s, errors.New("right arm code out of range")
    }
    if !(1 <= c[7] && c[7] <= len(bases)) {
        return s, errors.New("right arm code out of range")
    }
    s.setHat(hats[c[0]-1])
    s.setNose(noses[c[1]-1])
    s.setLeftEye(eyes[c[2]-1])
    s.setRightEye(eyes[c[3]-1])
    s.setLeftArm(leftArms[c[4]-1])
    s.setRightArm(rightArms[c[5]-1])
    s.setTorso(torsos[c[6]-1])
    s.setBase(bases[c[7]-1])
    return s, nil
}

func codeFromArgsOrRandom() (SnowmanCode, error) {
    if len(os.Args) > 1 {
        return snowmanCodeFromString(os.Args[1])
    } else {
        return randomCode(), nil
    }
}

func main() {
    rand.Seed(time.Now().UnixNano())
    code, err := codeFromArgsOrRandom()
    if err != nil {
        fmt.Println(err)
        return
    }
    s, err := SnowmanFromCode(code)
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println(s)
}
\$\endgroup\$
4
+50
\$\begingroup\$

Cute project ;-)


Do you see where copy-paste coding got the better of you?

if !(1 <= c[5] && c[5] <= len(rightArms)) {
    return s, errors.New("right arm code out of range")
}
if !(1 <= c[6] && c[6] <= len(torsos)) {
    return s, errors.New("right arm code out of range")
}
if !(1 <= c[7] && c[7] <= len(bases)) {
    return s, errors.New("right arm code out of range")
}

I meant the mocking jab at "copy-paste coding" is a joke. While in the above it may be possible to extract common elements and generalize the logic, that would be a slippery slope to over-engineering. And in any case, in Go, as far as I know, simple code with duplicated logic is preferred over complicated code.


I think you could eliminate getLine by converting the rune array to a string with string(line[:]):

func (s Snowman) String() string {
    var result string
    for i, line := range s {
        if i > 0 {
            result += "\n"
        }
        result += string(line[:])
    }
    return result
}

Of course this simplification with line[:] comes at the price of allocating a new slice. But given the scale of the task at hand, I think that's a reasonable compromise.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.