From 18a51fe0cfa3b20e98a10618d8a7930bac0d3133 Mon Sep 17 00:00:00 2001 From: "Andrew E. Torda" <torda@zbh.uni-hamburg.de> Date: Sat, 19 Feb 2022 16:24:58 +0100 Subject: [PATCH] Useful working version. --- Makefile | 9 +++ main_gfx.go | 1 + main_nogfx.go | 1 + mc_work/dorun.go | 1 - mc_work/mc_work.go | 2 +- mc_work/mc_work_test.go | 3 +- mc_work/plot.go | 2 +- mc_work/realmain_nogfx.go | 4 +- mc_work/set_suffix.go | 2 +- ui/ThePlan | 6 -- ui/mymain.go | 3 +- ui/output_tab.go | 115 +++++++++++++++++++++++++------- ui/param_tab.go | 136 ++++++++++++++++---------------------- ui/scrnplt.go | 80 ---------------------- ui/scrnplt_nogfx.go | 9 --- ui/ui_run.go | 34 +++++----- 16 files changed, 184 insertions(+), 224 deletions(-) delete mode 100644 ui/ThePlan delete mode 100644 ui/scrnplt.go delete mode 100644 ui/scrnplt_nogfx.go diff --git a/Makefile b/Makefile index 6268a46..3bff04f 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,8 @@ # Use the normal go build system for everything, but we want to automate some simple # commands. +.POSIX: + +LINTER=~/go/bin/linux_amd64/golangci-lint all: go build ./... @@ -8,6 +11,12 @@ test: go test ./... go test -tags no_gfx ./... +gofmt: + gofmt -s -w . + +lint: + $(LINTER) run + clean: go clean rm -rf */*_delme.* diff --git a/main_gfx.go b/main_gfx.go index db7a65b..df51889 100644 --- a/main_gfx.go +++ b/main_gfx.go @@ -1,4 +1,5 @@ // Aug 2021 +//go:build !no_gfx // +build !no_gfx // Ackley_mc is for playing with Monte Carlo or simulated annealing on the diff --git a/main_nogfx.go b/main_nogfx.go index adae628..9b09435 100644 --- a/main_nogfx.go +++ b/main_nogfx.go @@ -1,4 +1,5 @@ // Aug 2021 +//go:build no_gfx // +build no_gfx // Ackley_mc is for playing with Monte Carlo or simulated annealing on the diff --git a/mc_work/dorun.go b/mc_work/dorun.go index 3a2d7ce..b0fe2aa 100644 --- a/mc_work/dorun.go +++ b/mc_work/dorun.go @@ -31,7 +31,6 @@ func getSeed() int64 { seedLocker.Unlock() return r } -func breaker(...interface{}) {} type withBytes interface { Bytes() []byte diff --git a/mc_work/mc_work.go b/mc_work/mc_work.go index b3ad67f..9823996 100644 --- a/mc_work/mc_work.go +++ b/mc_work/mc_work.go @@ -26,6 +26,6 @@ var seed int64 // for random numbers, but each thread gets its own value // usage func usage() { - u := `[options] input_parameter_file` + u := `[options] [input_parameter_file]` fmt.Fprintf(flag.CommandLine.Output(), "usage of %s: %s\n", os.Args[0], u) } diff --git a/mc_work/mc_work_test.go b/mc_work/mc_work_test.go index 0a067f9..50d0d46 100644 --- a/mc_work/mc_work_test.go +++ b/mc_work/mc_work_test.go @@ -1,4 +1,5 @@ // Aug 2021 +//go:build no_gfx // +build no_gfx package mcwork @@ -88,7 +89,6 @@ func addOutNames(s gentest) string { return ret + "\n" } - func Test1(t *testing.T) { for _, s := range set1 { instring := addOutNames(s) @@ -106,7 +106,6 @@ func Test2(t *testing.T) { } } - func TestSetSuffix(t *testing.T) { var tdata = []struct { in, suffix, want string diff --git a/mc_work/plot.go b/mc_work/plot.go index e211260..f3373e9 100644 --- a/mc_work/plot.go +++ b/mc_work/plot.go @@ -170,7 +170,7 @@ func plotxWrt(cprm *cprm, ndim int) error { len_used := len(cprm.plotnstp) xdata := make([]f64, ndim) for i := 0; i < ndim; i++ { - xdata[i] = make([]float64, len_used, len_used) + xdata[i] = make([]float64, len_used) } var n int for i := 0; i < len_used; i++ { diff --git a/mc_work/realmain_nogfx.go b/mc_work/realmain_nogfx.go index 8551c43..81e7f10 100644 --- a/mc_work/realmain_nogfx.go +++ b/mc_work/realmain_nogfx.go @@ -1,15 +1,15 @@ // feb 2022 // We are building a version without graphics +//go:build no_gfx // +build no_gfx - // This should mirror the file realmain_gfx.go. It should also remain short. package mcwork import ( + "fmt" "io" "os" - "fmt" ) // realmain is the real main function. The program wants to read from a file, diff --git a/mc_work/set_suffix.go b/mc_work/set_suffix.go index 5428c20..154dd25 100644 --- a/mc_work/set_suffix.go +++ b/mc_work/set_suffix.go @@ -49,4 +49,4 @@ func removeQuotes(s string) string { return s } -func nothing (...interface{}){} +func nothing(...interface{}) {} diff --git a/ui/ThePlan b/ui/ThePlan deleted file mode 100644 index 8516e10..0000000 --- a/ui/ThePlan +++ /dev/null @@ -1,6 +0,0 @@ -* get a plot on the screen -* get two plots on the screen - -* Give the plotting function a bytes.buffer to scribble into. If we are writing to a file, dump it there. If we are drawing pictures, return it. - -See if we can keep this a bit clean and have a build tag that works without the gui, as well as an option that turns off the gui. \ No newline at end of file diff --git a/ui/mymain.go b/ui/mymain.go index 27bc825..e17ff98 100644 --- a/ui/mymain.go +++ b/ui/mymain.go @@ -1,5 +1,6 @@ // feb 2022 // We are building a version without graphics +//go:build !no_gfx // +build !no_gfx package ui @@ -8,7 +9,7 @@ import ( "fmt" "io" "os" - + "example.com/ackley_mc/mc_work" ) diff --git a/ui/output_tab.go b/ui/output_tab.go index 41a44f4..261b813 100644 --- a/ui/output_tab.go +++ b/ui/output_tab.go @@ -1,43 +1,57 @@ // 17 Feb 2022 -// Handle the output tab stuff +// Output tab / results + +//go:build !no_gfx +// +build !no_gfx // There should be three states. -// 1. initial, nothing happened yet -// 2. thinking +// 1. initial, nothing happened yet, but we do not need a signal for this +// 2. calculating, busy // 3. show results +// 4. an error occurred package ui import ( "bytes" + "os" "fyne.io/fyne/v2" "fyne.io/fyne/v2/canvas" - "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/dialog" + "fyne.io/fyne/v2/storage" "fyne.io/fyne/v2/widget" ) +// A message is sent to the output tab, telling us the current status type status uint8 const ( - initial status = iota - calculating + calculating status = iota resultsReady + errorCalc ) type workstatus struct { fdata, xdata []byte + err error status status } -// showIniTab is just a card to be shown before we have any calculations -// done. +// showIniTab is a card to be shown before we have any calculations. func showIniTab(cntr *fyne.Container) { cntr.Add(widget.NewCard("No results yet", "go back to the input", nil)) } -// emptyContainer gets rid of any old contents -func emptyContainer (cntr *fyne.Container) { +// If we get a message with an error, display it +func showErrTab(cntr *fyne.Container, err error) { + cntr.Add(widget.NewCard("ERROR in MC", err.Error(), nil)) +} + +// emptyContainer gets rid of any old contents. Could I just say, +// cntr.Objects = nil ? +func emptyContainer(cntr *fyne.Container) { for _, o := range cntr.Objects { cntr.Remove(o) } @@ -46,36 +60,89 @@ func emptyContainer (cntr *fyne.Container) { // showCalcTab is shown while calculating. Have to check if the refresh() // is actually necessary. func showCalcTab(cntr *fyne.Container) { - emptyContainer (cntr) + emptyContainer(cntr) cntr.Add(widget.NewCard("calculating", "busy", nil)) cntr.Refresh() } -// showResultsTab show the two plots -func showResultsTab(cntr *fyne.Container, fdata, xdata []byte) { - emptyContainer (cntr) +// fwrt writes the file. It will be called by the filesave dialog +func fwrt(io fyne.URIWriteCloser, err error, d []byte, parent fyne.Window) { + if io == nil { + return // it was cancelled + } + if err != nil { + dialog.ShowError(err, parent) + return + } + defer io.Close() + if _, err := io.Write(d); err != nil { + dialog.ShowError(err, parent) + } +} + +// innerWrite will be called by the button to save a file +func innerWrite(d []byte, parent fyne.Window) { + fwrt := func(io fyne.URIWriteCloser, err error) { fwrt(io, err, d, parent) } + t := dialog.NewFileSave(fwrt, parent) + t.SetFilter(storage.NewExtensionFileFilter([]string{"png", "PNG"})) + if cwd, err := os.Getwd(); err == nil { + if y, err := storage.ListerForURI(storage.NewFileURI(cwd)); err == nil { + t.SetLocation(y) // on error, just use default location + } + } + + t.Show() +} + +// leftbar sets up the buttons on the left +func leftbar(win fyne.Window, fdata, xdata []byte) *fyne.Container { + wrtFdata := func() { innerWrite(fdata, win) } + wrtXdata := func() { innerWrite(xdata, win) } + fdataBtn := widget.NewButton("save func\nvalue plot\nto file", wrtFdata) + xdataBtn := widget.NewButton("save\nX coord plot\nto file", wrtXdata) + return container.NewVBox(fdataBtn, xdataBtn) +} + +// png2image takes the file data we have and puts it in a fyne image with +// a size we want. fname is used by fyne to recognise the file type. +func png2image(d []byte, fname string) *canvas.Image { pictureSize := fyne.Size{Width: 500, Height: 250} - fImage := canvas.NewImageFromReader(bytes.NewReader(fdata), "func.png") - fImage.FillMode = canvas.ImageFillContain - fImage.SetMinSize( pictureSize) - xImage := canvas.NewImageFromReader(bytes.NewReader(xdata), "xdata.png") - xImage.FillMode = canvas.ImageFillContain - xImage.SetMinSize( pictureSize) - content := container.NewGridWithRows(2, fImage, xImage) - cntr.Add(content) + image := canvas.NewImageFromReader(bytes.NewReader(d), fname) + image.FillMode = canvas.ImageFillContain + image.SetMinSize(pictureSize) + return image +} + +// showResultsTab show the two plots +func showResultsTab(cntr *fyne.Container, win fyne.Window, fdata, xdata []byte) { + emptyContainer(cntr) + fImage := png2image(fdata, "function.png") + xImage := png2image(xdata, "xdata.png") + + right := container.NewGridWithRows(2, fImage, xImage) + left := leftbar(win, fdata, xdata) + box := container.NewHBox(left, right) + cntr.Add(box) } // outputTab is run as a background process. After showing the initial // screen, it sits and waits on notifications. When it gets one, // it redraws its tab. -func outputTab(chn chan workstatus, cntr *fyne.Container) { +func outputTab(chn chan workstatus, cntr *fyne.Container, form *widget.Form, win fyne.Window) { showIniTab(cntr) + breaker() for s := range chn { switch s.status { case calculating: showCalcTab(cntr) case resultsReady: - showResultsTab(cntr, s.fdata, s.xdata) + showResultsTab(cntr, win, s.fdata, s.xdata) + breaker() + form.Enable() + form.Refresh() + case errorCalc: + showErrTab(cntr, s.err) + form.Enable() } } } diff --git a/ui/param_tab.go b/ui/param_tab.go index c38f127..a53c609 100644 --- a/ui/param_tab.go +++ b/ui/param_tab.go @@ -9,11 +9,11 @@ package ui import ( "fmt" "fyne.io/fyne/v2" - "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/data/binding" "fyne.io/fyne/v2/dialog" "fyne.io/fyne/v2/storage" "fyne.io/fyne/v2/widget" + "os" "strconv" "strings" @@ -58,25 +58,27 @@ func validateXini(s string) error { return err } -// paramScreen sets up a screen full of parameters we can adjust. +// floatitem gives us a form item, but also a function to refresh the item. +// This gets put on the list of things to do from the reload function. +func floatItem(xAddr *float64, label string) (*widget.FormItem, func()) { + bind := binding.BindFloat(xAddr) + entry := widget.NewEntryWithData(binding.FloatToStringWithFormat(bind, "%10.2f")) + fitem := widget.NewFormItem(label, entry) + reloadfunc := func() { bind.Reload() } + return fitem, reloadfunc +} + +// paramBox sets up a screen full of parameters we can adjust. // It returns the screen and a function to be called to refresh it. // This is necessary, since there is a button to read parameters from // a file, which means all the values should be updated. -func paramScreen(mcPrm *mcwork.McPrm) (*fyne.Container, func()) { - iniTmp := binding.BindFloat(&mcPrm.IniTmp) - iniTmpEntry := widget.NewEntryWithData(binding.FloatToString(iniTmp)) - iniTmpLabel := widget.NewLabel("initial temp") - - fnlTmp := binding.BindFloat(&mcPrm.FnlTmp) - fnlTmpEntry := widget.NewEntryWithData(binding.FloatToString(fnlTmp)) - fnlTmpLabel := widget.NewLabel("final temp") - - xDlta := binding.BindFloat(&mcPrm.XDlta) - xDltaEntry := widget.NewEntryWithData(binding.FloatToString(xDlta)) - xDltaLabel := widget.NewLabel("X delta") +func paramBox(mcPrm *mcwork.McPrm, parent fyne.Window, chn chan workstatus) *widget.Form { + iniTmpItem, iniTmpReload := floatItem(&mcPrm.IniTmp, "initial temperature") + fnlTmpItem, fnlTmpReload := floatItem(&mcPrm.FnlTmp, "final temperature") + xDltaItem, xDltaReload := floatItem(&mcPrm.XDlta, "X delta") xIniStr := binding.NewString() - xIniStr.Set(fslicestrng(mcPrm.XIni)) + _ = xIniStr.Set(fslicestrng(mcPrm.XIni)) xIniEntry := widget.NewEntryWithData(xIniStr) xIniEntry.OnChanged = func(s string) { x, err := s2f32(s) @@ -87,51 +89,37 @@ func paramScreen(mcPrm *mcwork.McPrm) (*fyne.Container, func()) { } } xIniEntry.Validator = validateXini - xIniLabel := widget.NewLabel("X ini") + xIniItem := widget.NewFormItem("initial X", xIniEntry) nStepBnd := binding.BindInt(&mcPrm.NStep) nStepEntry := widget.NewEntryWithData(binding.IntToString(nStepBnd)) - nStepLabel := widget.NewLabel("num steps") - - r := container.NewGridWithColumns(2, - iniTmpLabel, iniTmpEntry, - fnlTmpLabel, fnlTmpEntry, - xDltaLabel, xDltaEntry, - xIniLabel, xIniEntry, - nStepEntry, nStepLabel) - refreshPScreen := func() { - iniTmp.Reload() - fnlTmp.Reload() - xDlta.Reload() + nStepItem := widget.NewFormItem("N steps", nStepEntry) + + reloadPTab := func() { + iniTmpReload() + fnlTmpReload() + xDltaReload() xIniEntry.SetText(fslicestrng(mcPrm.XIni)) - nStepBnd.Reload() + _ = nStepBnd.Reload() } - return r, refreshPScreen -} + rdfile := func() { rdwork(mcPrm, parent, reloadPTab) } + rdfileBtn := widget.NewButton("get file ", rdfile) + rdFileItem := widget.NewFormItem("read from a file", rdfileBtn) -// checkOk does a last pass over all fields and calls any validators -// it can find. A run can only be started if everything seems to be OK. -func checkOk(c *fyne.Container, parent fyne.Window) error { - for _, obj := range c.Objects { - if ent, ok := obj.(*widget.Entry); ok { - if err := ent.Validate(); err != nil { - dialog.NewError(err, parent).Show() - return err - } - } - } - return nil + r := widget.NewForm( + iniTmpItem, fnlTmpItem, xDltaItem, xIniItem, nStepItem, rdFileItem) + r.SubmitText = "start calculation" + r.OnSubmit = func() { startrun(chn, parent, r, mcPrm) } + return r } // rdwork is called after the file open dialog gets "OK" -func rdwork(mcPrm *mcwork.McPrm, parent fyne.Window, refreshme func()) error { - var e error +func rdwork(mcPrm *mcwork.McPrm, parent fyne.Window, reloadPTab func()) { getmcprm := func(rd fyne.URIReadCloser, err error) { if err != nil { fmt.Println("error given to getmcprm") dialog.NewError(err, parent).Show() - e = err return } if rd == nil { // cancelled @@ -139,12 +127,11 @@ func rdwork(mcPrm *mcwork.McPrm, parent fyne.Window, refreshme func()) error { } defer rd.Close() if err := mcwork.RdPrm(rd, mcPrm); err != nil { - e = err + dialog.NewError(err, parent).Show() return } else { - fmt.Println("I think mcprm is", mcPrm) - refreshme() dialog.NewInformation("OK", "read file, no errors", parent).Show() + reloadPTab() return } } @@ -156,37 +143,30 @@ func rdwork(mcPrm *mcwork.McPrm, parent fyne.Window, refreshme func()) error { t.SetLocation(y) // If there was an error, we just use default location } } - t.Show() // set an error and see if anybody notices - return e } -// inputTab sets up the input page. At the top, we have a button to read from -// a file. -func inputTab(mcPrm *mcwork.McPrm, parent fyne.Window, chn chan workstatus) (*fyne.Container, error) { - paramBinding, refreshPscreen := paramScreen(mcPrm) - rdfile := func() { - if err := rdwork(mcPrm, parent, refreshPscreen); err != nil { - dialog.NewError(err, parent).Show() - } +// mcWrap is run in the background so the interface does not block. +// It runs the Monte Carlo, then when it is finished, sends a message +// down the channel. +func mcWrap(chn chan workstatus, parent fyne.Window, mcPrm *mcwork.McPrm) { + chn <- workstatus{status: calculating} + if fdata, xdata, err := mcwork.DoRun(mcPrm); err != nil { + dialog.NewError(err, parent).Show() + chn <- workstatus{status: errorCalc, err: err} + } else { + chn <- workstatus{fdata: fdata, xdata: xdata, status: resultsReady} } - startrun := func() { - if err := checkOk(paramBinding, parent); err != nil { - dialog.NewError(err, parent).Show() - return - } - chn <- workstatus{status: calculating} - if fdata, xdata, err := mcwork.DoRun(mcPrm); err != nil { - dialog.NewError(err, parent).Show() - close(chn) - return - } else { - chn <- workstatus{fdata: fdata, xdata: xdata, status: resultsReady} - } - } - rdfileBtn := widget.NewButton("read file", rdfile) - runBtn := widget.NewButton("start run", startrun) - buttons := container.NewVBox(rdfileBtn, runBtn) - c := container.NewHBox(buttons, paramBinding) - return c, nil +} + +// startrun is what happens when you click on the button to start +// a calculation. +func startrun(chn chan workstatus, parent fyne.Window, form *widget.Form, mcPrm *mcwork.McPrm) { + form.Disable() + go mcWrap(chn, parent, mcPrm) +} + +// inputTab sets up the input page. +func inputTab(mcPrm *mcwork.McPrm, parent fyne.Window, chn chan workstatus) *widget.Form { + return paramBox(mcPrm, parent, chn) } diff --git a/ui/scrnplt.go b/ui/scrnplt.go deleted file mode 100644 index ac34c13..0000000 --- a/ui/scrnplt.go +++ /dev/null @@ -1,80 +0,0 @@ -// 25 Jan 2020 -// Given a buffer or two with a plot picture, send it to the screen - -// +build !no_gfx - -package ui - -import ( - "bytes" - "fmt" - - "fyne.io/fyne/v2" - "fyne.io/fyne/v2/canvas" - "fyne.io/fyne/v2/container" - "fyne.io/fyne/v2/dialog" - "fyne.io/fyne/v2/storage" -) - -func tryfsave(uriw fyne.URIWriteCloser, err error, data []byte, parent fyne.Window) { - if err != nil { - dialog.ShowError(err, parent) - return - } - if uriw == nil { - return // cancel - } - if _, err := uriw.Write(data); err != nil { - fmt.Println("error writing") - } - if err := uriw.Close(); err != nil { - fmt.Println("error closing") - } -} - -func fsave(parent fyne.Window, fdata []byte, suggested string) { - filter := storage.NewExtensionFileFilter([]string{"png", "PNG"}) - ts := func(uriw fyne.URIWriteCloser, err error) { - tryfsave(uriw, err, fdata, parent) - } - t := dialog.NewFileSave(ts, parent) - t.SetFileName(suggested) - t.SetFilter(filter) - t.Show() -} - -func topmenu(parent fyne.Window, fdata, xdata []byte) *fyne.Menu { - ds := func() { fsave(parent, fdata, "func_val.png") } // function values - xs := func() { fsave(parent, xdata, "x_trj.png") } - c := fyne.NewMenuItem("save func plot", ds) - d := fyne.NewMenuItem("save X trajectory plot", xs) - return fyne.NewMenu("actions", c, d) -} - -func fileSaved(f fyne.URIWriteCloser, w fyne.Window) { - defer f.Close() - _, err := f.Write([]byte("Written by Fyne demo\n")) - if err != nil { - dialog.ShowError(err, w) - } - err = f.Close() - if err != nil { - dialog.ShowError(err, w) - } - fmt.Println("Saved to...", f.URI()) -} - -func nothing(...interface{}) {} -func breaker(...interface{}) {} -func scrnplt(fdata, xdata []byte) *fyne.Container { - fImage := canvas.NewImageFromReader(bytes.NewReader(fdata), "func.png") - fImage.FillMode = canvas.ImageFillContain - xImage := canvas.NewImageFromReader(bytes.NewReader(xdata), "xdata.png") - xImage.FillMode = canvas.ImageFillContain - content := container.NewGridWithRows(2, fImage, xImage) - -// w.Resize(fyne.NewSize(500, 600)) -// w.SetContent(content) - // w.ShowAndRun() - return content -} diff --git a/ui/scrnplt_nogfx.go b/ui/scrnplt_nogfx.go deleted file mode 100644 index aa5e821..0000000 --- a/ui/scrnplt_nogfx.go +++ /dev/null @@ -1,9 +0,0 @@ -// +build no_gfx -// -// If we do not have graphics - -package ui - -func Scrnplt(fdata []byte, xdata []byte) { - print ("No graphics") -} diff --git a/ui/ui_run.go b/ui/ui_run.go index ef2ff0a..a84062d 100644 --- a/ui/ui_run.go +++ b/ui/ui_run.go @@ -1,12 +1,12 @@ // 25 Jan 2020 // Given a buffer or two with a plot picture, send it to the screen +//go:build !no_gfx // +build !no_gfx package ui import ( - "fyne.io/fyne/v2" "fyne.io/fyne/v2/app" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/widget" @@ -14,30 +14,28 @@ import ( "example.com/ackley_mc/mc_work" ) - -func initIn () *widget.Card { - return widget.NewCard("input screen", "click on run to start a calc", nil) -} - -func UiDoRun (mcPrm *mcwork.McPrm) error { +func UiDoRun(mcPrm *mcwork.McPrm) error { a := app.NewWithID("Monte Carlo") - w := a.NewWindow ("Monte Carlo") - quitbutton := widget.NewButton ("quit .. click somwhere in here", a.Quit) + w := a.NewWindow("Monte Carlo") + quitbutton := widget.NewButton("click somewhere in here to exit", a.Quit) chn := make(chan workstatus) - inputTabCallback := func() (*fyne.Container) { - a, _ := inputTab(mcPrm, w, chn) - return a - } - t1 := container.NewTabItem("input tab", inputTabCallback()) - cntrOut := fyne.NewContainer() + // inputTabCallback := func() *widget.Form { return (inputTab(mcPrm, w, chn))} + inputForm := inputTab(mcPrm, w, chn) + t1 := container.NewTabItem("input tab", inputForm) + cntrOut := container.NewWithoutLayout() t2 := container.NewTabItem("output tab", cntrOut) t3 := container.NewTabItem("quit me", quitbutton) appTab := container.NewAppTabs(t1, t2, t3) w.SetContent(appTab) - breaker() - go outputTab(chn, cntrOut) + go outputTab(chn, cntrOut, inputForm, w) w.ShowAndRun() - close (chn) + close(chn) return nil } + +// nothing does nothing +func nothing(...interface{}) {} + +// and breaker does not do much more +func breaker(...interface{}) {} -- GitLab