Finished C++ and basic examples
* Ready to talk!
This commit is contained in:
parent
e59dac99bf
commit
ad3f5956bd
7 changed files with 385 additions and 32 deletions
22
Notes.md
22
Notes.md
|
@ -52,19 +52,19 @@ html header: <link rel="stylesheet"
|
|||
```
|
||||
+ currying: not really, but binding via lambdas or functools.partial() or
|
||||
https://mtomassoli.wordpress.com/2012/03/18/currying-in-python/
|
||||
- decorators!
|
||||
- still FP has advantages and is heavily used, i.e. in genomics (works on
|
||||
+ decorators!
|
||||
+ still FP has advantages and is heavily used, i.e. in genomics (works on
|
||||
tons of lengthy lists)
|
||||
* FunCtional++: On the fast lane
|
||||
- "Classical" C++ has some functional stuff, bust i.e. no lambdas (hardly usable)
|
||||
- Changed with the new C++11-standard
|
||||
- Buzzwords:
|
||||
- `map` defines a Datatype in C++!
|
||||
- lambdas in C++
|
||||
* FunC++tional: On the fast lane
|
||||
+ "Classical" C++ has some functional stuff, bust i.e. no lambdas (hardly usable)
|
||||
+ Changed with the new C++11-standard
|
||||
+ Buzzwords:
|
||||
+ `map` defines a Datatype in C++!
|
||||
+ lambdas in C++
|
||||
```[](int x, int y) { return a<b;} ;```
|
||||
[] can be used to capture variables, i.e. by reference or value
|
||||
- `std::for_each` from `algorithm`: Apply `void fun(T &a)` to iterator
|
||||
+ `std::for_each` from `algorithm`: Apply `void fun(T &a)` to iterator
|
||||
containing `T` values
|
||||
- `std::transform` from `algorithm`: same as for_each, but stores return
|
||||
+ `std::transform` from `algorithm`: same as for_each, but stores return
|
||||
value in another range
|
||||
- `std::accumulate` from `numeric`: Wants binary operation, i.e. `std::minus<int>`
|
||||
+ `std::accumulate` from `numeric`: Wants binary operation, i.e. `std::minus<int>`
|
||||
|
|
31
examples/02_python.py
Normal file
31
examples/02_python.py
Normal file
|
@ -0,0 +1,31 @@
|
|||
#!/usr/bin/env python3
|
||||
import sys
|
||||
|
||||
|
||||
def debug(func):
|
||||
def inner(*args, **kwargs):
|
||||
sys.stderr.write("F: {}, args: {}, kwargs: {}\n"
|
||||
.format(func.__name__, args, kwargs))
|
||||
return func(*args, **kwargs)
|
||||
return inner
|
||||
|
||||
|
||||
@debug
|
||||
def foo(x):
|
||||
pass
|
||||
|
||||
|
||||
def mybubblesort(array, func=lambda x, y: True if x > y else False):
|
||||
if (len(array) == 0):
|
||||
return []
|
||||
else:
|
||||
x, *xs = array
|
||||
return mybubblesort([y for y in xs if func(x, y)], func) \
|
||||
+ [x] \
|
||||
+ mybubblesort([y for y in xs if not func(x, y)], func)
|
||||
|
||||
if __name__ == "__main__":
|
||||
foo(2)
|
||||
a = [2,5,12,4,1,0]
|
||||
print(mybubblesort(a))
|
||||
print(mybubblesort(a, lambda x, y: True if x < y else False))
|
50
examples/03_cpp.cpp
Normal file
50
examples/03_cpp.cpp
Normal file
|
@ -0,0 +1,50 @@
|
|||
#include <iostream>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include <numeric>
|
||||
|
||||
int main() {
|
||||
std::vector<int> a{1,2,3,4};
|
||||
std::vector<int> b{5,6,7,8};
|
||||
// result vector
|
||||
std::vector<double> c(a.size(), 0);
|
||||
|
||||
// double
|
||||
std::for_each(a.begin(), a.end(), [](int &n){ n*=2;});
|
||||
|
||||
std::cout << "Double:" << std::endl;
|
||||
for (auto const &i: a){ std::cout<<i<<" ";}
|
||||
std::cout << std::endl;
|
||||
|
||||
// Reset a
|
||||
a = {1,2,3,4};
|
||||
|
||||
// inverse
|
||||
std::transform(a.begin(), a.end(),
|
||||
c.begin(),
|
||||
[](int i){return 1.0/i;});
|
||||
|
||||
std::cout << "Multiplicative inverse:" << std::endl;
|
||||
for (auto const &i: c){ std::cout << i << " ";}
|
||||
std::cout << std::endl;
|
||||
|
||||
// reziprocal sum
|
||||
std::transform(a.begin(), a.end(),
|
||||
b.begin(), c.begin(),
|
||||
[](int i, int j){return 1.0/(1.0/i + 1.0/j);});
|
||||
std::cout << "Reziprocal sum:" << std::endl;
|
||||
for (auto const &i: c){ std::cout << i << " ";}
|
||||
std::cout << std::endl;
|
||||
|
||||
// Folds
|
||||
int sum = std::accumulate(a.begin(), a.end(), 0);
|
||||
int diff = std::accumulate(a.begin(), a.end(), 15, std::minus<int>());
|
||||
int mult = std::accumulate(a.begin(), a.end(), 1,
|
||||
[](int a, int b)->int { return a * b; });
|
||||
std::cout << "Sum: " << sum
|
||||
<< "\nDiff from 15: " << diff
|
||||
<< "\nProd: " << mult
|
||||
<< std::endl;
|
||||
|
||||
return 0;
|
||||
}
|
32
examples/04_cpp17.cpp
Normal file
32
examples/04_cpp17.cpp
Normal file
|
@ -0,0 +1,32 @@
|
|||
#include <iostream>
|
||||
// compile using std=c++1z-option
|
||||
// tested with clang++ and g++
|
||||
// using:
|
||||
// $CC -Wall -Werror -pedantic -pedantic-errors -std=c++1z -o $OUT $IN
|
||||
|
||||
/* "Old" code:
|
||||
* The usage of auto to implicitly get the return type is C++17,
|
||||
* the main part is using C++11 variadic templates and overloading
|
||||
*/
|
||||
auto sum1() { return 0;}
|
||||
|
||||
template<typename T>
|
||||
auto sum1(T t) { return t; }
|
||||
|
||||
template<typename T, typename... Ts>
|
||||
auto sum1(T t, Ts... ts) {return t + sum1(ts...);}
|
||||
|
||||
/* New code:
|
||||
* Using c++17 fold expressions, this gets way shorter and
|
||||
* less error prone
|
||||
*/
|
||||
template <typename... T >
|
||||
auto sum2 (T... args){
|
||||
return (... + args);
|
||||
}
|
||||
|
||||
int main() {
|
||||
std::cout << sum1(1,2,3,4,5,6,7,8,9,10) << std::endl;
|
||||
std::cout << sum2(1,2,3,4,5,6,7,8,9,10) << std::endl;
|
||||
return 0;
|
||||
}
|
|
@ -3,6 +3,8 @@
|
|||
\usepackage{upquote}
|
||||
\usepackage[section, cache=true,]{minted}
|
||||
|
||||
\usemintedstyle{manni}
|
||||
|
||||
\newminted[ccode]{c}%
|
||||
{
|
||||
linenos=true,
|
||||
|
@ -58,5 +60,5 @@
|
|||
|
||||
%%% Local Variables:
|
||||
%%% mode: latex
|
||||
%%% TeX-master: "../linux-script"
|
||||
%%% TeX-master: "../wtfunctional"
|
||||
%%% End:
|
||||
|
|
30
tex/wtf.bib
30
tex/wtf.bib
|
@ -1,11 +1,29 @@
|
|||
@online{whichfold,
|
||||
title={Foldr Foldl Foldl' - HaskellWiki},
|
||||
urldate={2016-04-21},
|
||||
url={https://wiki.haskell.org/Foldr_Foldl_Foldl%27},
|
||||
title = {Foldr Foldl Foldl' - HaskellWiki},
|
||||
url = {wiki.haskell.org/Foldr_Foldl_Foldl},
|
||||
}
|
||||
|
||||
@online{decorators,
|
||||
title={simeonfranklin.com - Understanding Python Decorators in 12 Easy Steps!},
|
||||
urldate={2016-04-25},
|
||||
url={http://simeonfranklin.com/blog/2012/jul/1/python-decorators-in-12-steps/},
|
||||
title = {simeonfranklin.com - Understanding Python Decorators in 12 Easy Steps!},
|
||||
url = {simeonfranklin.com/blog/2012/jul/1/python-decorators-in-12-steps/},
|
||||
}
|
||||
|
||||
@Online{cppiter,
|
||||
title = {C++ concepts: Iterator - cppreference.com},
|
||||
url = {en.cppreference.com/w/cpp/concept/Iterator},
|
||||
}
|
||||
|
||||
@online{generics,
|
||||
title = {Index of /~kami/2015-32C3/},
|
||||
url = {people.freebsd.org/~kami/2015-32C3/},
|
||||
}
|
||||
|
||||
@online{cpp17,
|
||||
title = {C++17 content prediction (pre-Jacksonville and post-Kona report) – Michael Wong's Standard},
|
||||
url = {https://wongmichael.com/2016/02/28/c17-content-predictionpre-jacksonville-and-post-kona-report/},
|
||||
}
|
||||
|
||||
@online{cppfolds,
|
||||
title = {Fold expressions},
|
||||
url = {www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4191.html},
|
||||
}
|
|
@ -5,15 +5,17 @@
|
|||
\usepackage{tabularx}
|
||||
\usepackage[backend=biber,]{biblatex}
|
||||
\bibliography{wtf}
|
||||
\renewcommand{\bibfont}{\small}
|
||||
|
||||
\usepackage{fontspec}
|
||||
\setsansfont{Fira Sans}
|
||||
\setmonofont{Inconsolata-g}
|
||||
|
||||
\usetheme{Antibes}
|
||||
%\usecolortheme{beaver}
|
||||
\setbeamercovered{transparent}
|
||||
|
||||
\newcommand{\cpp}{\texttt{C++}}
|
||||
|
||||
\title{WTFunctional}
|
||||
\author{Oliver Rümpelein}
|
||||
\subtitle{Using functional structures in non-functional languages}
|
||||
|
@ -23,6 +25,17 @@
|
|||
\begin{document}
|
||||
\frame{\titlepage}
|
||||
|
||||
\begin{frame}[plain]{What?}
|
||||
\begin{enumerate}[<+->]
|
||||
\item Dafunc? Introduction to functional paradigms using Haskell
|
||||
\item PhuncY! Functional programming in Python
|
||||
\item Fun\cpp{}tional: STL-hacks and usage in \cpp
|
||||
\end{enumerate}
|
||||
\begin{uncoverenv}<4-| invisible@1-3>
|
||||
\emph{With preview to \cpp{}17/20/22!}
|
||||
\end{uncoverenv}
|
||||
\end{frame}
|
||||
|
||||
\section{Dafunc?}
|
||||
\subsection{Functional programming}
|
||||
\begin{frame}{Understanding functional paradigms}
|
||||
|
@ -169,7 +182,7 @@ c = addto b 4
|
|||
\end{frame}
|
||||
|
||||
\begin{frame}[fragile]{Folds (2)}
|
||||
\uncover<+-> Example: Self written Right fold and sum:
|
||||
\uncover<+-> Example: Self written right fold and sum:
|
||||
\begin{haskell}
|
||||
mfold f z [] = z
|
||||
mfold f z (x:xs) = f x (mfold f z xs)
|
||||
|
@ -185,9 +198,9 @@ g = msum [1..100]
|
|||
Get all Pythagorean triangles with a hypotenuse off length at most 15:
|
||||
\begin{haskell}
|
||||
> [(a,b,c) | a <- [1..15],
|
||||
b <- [1..a],
|
||||
c <- [1..b],
|
||||
a^2 == b^2 + c^2]
|
||||
b <- [1..a],
|
||||
c <- [1..b],
|
||||
a^2 == b^2 + c^2]
|
||||
[(5,4,3),(10,8,6),(13,12,5),(15,12,9)]
|
||||
\end{haskell}
|
||||
\end{frame}
|
||||
|
@ -197,8 +210,8 @@ a^2 == b^2 + c^2]
|
|||
\begin{haskell}
|
||||
bsort f [] = []
|
||||
bsort f (x:xs) = (bsort f a) ++ [x] ++ (bsort f b)
|
||||
where a = [ y | y <- xs, not (f x y) ]
|
||||
b = [ y | y <- xs, (f x y) ]
|
||||
where a = [ y | y <- xs, not (f x y) ]
|
||||
b = [ y | y <- xs, (f x y) ]
|
||||
mbsort = bsort (\x y -> (x > y))
|
||||
\end{haskell}
|
||||
\pause Result:
|
||||
|
@ -209,7 +222,7 @@ mbsort = bsort (\x y -> (x > y))
|
|||
\end{haskell}
|
||||
\end{frame}
|
||||
|
||||
\section{Phuncy!}
|
||||
\section{PhuncY!}
|
||||
\subsection{Overview}
|
||||
\begin{frame}{Functional programming in Python}
|
||||
\begin{itemize}[<+->]
|
||||
|
@ -222,6 +235,7 @@ mbsort = bsort (\x y -> (x > y))
|
|||
\end{itemize}
|
||||
\end{frame}
|
||||
|
||||
\subsection{Elements}
|
||||
\begin{frame}[fragile]{Lambdas, Maps}
|
||||
\begin{itemize}[<+->]
|
||||
\item Lambda-syntax: \pycmd{lambda a,b: a+b}
|
||||
|
@ -286,27 +300,233 @@ bar(3) # 5
|
|||
\end{itemize}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}[fragile]{Decorators}
|
||||
\begin{frame}{Decorators (1)}
|
||||
\begin{itemize}[<+->]
|
||||
\item Often used to modify functions in Frameworks
|
||||
\item Encapsulates other functions. More infos at \cite{decorators}
|
||||
\item Basic pattern: Decorator is a function that itself takes a function,
|
||||
and returns a wrapper
|
||||
\item Step-by-step introduction to decorators at~\cite{decorators}
|
||||
\end{itemize}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}[fragile]{Decorators (2)}
|
||||
\begin{pycode}
|
||||
def debug(func):
|
||||
def inner(*args, **kwargs):
|
||||
sys.stderr.write("F: {}, args: {}, kwargs {}".format(func.__name__, args,
|
||||
kwargs))
|
||||
return func(*args, **args)
|
||||
print("F: {}, args: {}, kwargs: {}\n"
|
||||
.format(func.__name__, args, kwargs))
|
||||
return func(*args, **kwargs)
|
||||
return inner
|
||||
|
||||
@debug
|
||||
def foo(x):
|
||||
print(x)
|
||||
pass
|
||||
|
||||
foo(2) # => F: foo, args: (2), kwargs: {}
|
||||
\end{pycode}
|
||||
\end{frame}
|
||||
|
||||
\subsection{Conclusion}
|
||||
\begin{frame}{Quite enough…}
|
||||
\begin{itemize}[<+->]
|
||||
\item Python is not really functional…
|
||||
\item …but is strongly influenced by functional paradigms.
|
||||
\item Its functional parts are heavily used, i.e in Genomics
|
||||
\end{itemize}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}[fragile]{Example}
|
||||
\begin{pycode}
|
||||
def mybubblesort(array,
|
||||
func=lambda x, y: True if x > y else False):
|
||||
if (len(array) == 0):
|
||||
return []
|
||||
else:
|
||||
x, *xs = array
|
||||
return mybubblesort([y for y in xs
|
||||
if func(x,y)], func) \
|
||||
+ [x] \
|
||||
+ mybubblesort([y for y in xs \
|
||||
if not func(x,y)], func)
|
||||
\end{pycode}
|
||||
\end{frame}
|
||||
|
||||
\section{Fun\cpp{}ional}
|
||||
\subsection{Overview}
|
||||
|
||||
\begin{frame}{Functional programming in \cpp{}}
|
||||
\begin{itemize}[<+->]
|
||||
\item \enquote{Classical} \cpp{} has a few functional elements…
|
||||
\item …but lacks lambdas, for instance.
|
||||
\item This changed with the modern standards, starting from \cpp{}11.
|
||||
\end{itemize}
|
||||
\end{frame}
|
||||
|
||||
|
||||
\subsection{Elements}
|
||||
\begin{frame}[fragile]{Lists}
|
||||
\begin{itemize}[<+->]
|
||||
\item In \cpp{}, \emph{Iterators} are equivalent to lists in functional languages.
|
||||
\item Examples of iterators include \cppcmd{vector} and \cppcmd{array}.
|
||||
\item See~\cite{cppiter} for more information about the specific iterator
|
||||
types and the prerequisites they bring.
|
||||
\end{itemize}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}[fragile]{lambdas}
|
||||
\begin{itemize}[<+->]
|
||||
\item \emph{Lambdas} have been introduced with \cpp{}11
|
||||
\item Syntax:
|
||||
\begin{cppcode}
|
||||
[v1,&v2](int v1, int v2)
|
||||
{return v1 < v2}
|
||||
\end{cppcode}
|
||||
\item The \cppcmd{[]} denotes the capture-list and specifies, whether
|
||||
variables are used by value or by reference. If this is empty,
|
||||
anything is used by value.
|
||||
\item Lambdas are fully-featured \emph{functionals}, such are functions
|
||||
wrapped with \cppcmd{std::function}, and objects implementing
|
||||
\cppcmd{operator()}.
|
||||
\end{itemize}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}[fragile]{Maps (1)}
|
||||
\uncover<+->{\begin{alertblock}{map ≠ map}
|
||||
\cppcmd{std::map} is a data-type similar to pythons \pycmd{dict} and has no
|
||||
relation to the functional concept of maps!
|
||||
\end{alertblock}}
|
||||
\uncover<+->{The following can be used instead (both from \cppcmd{<algorithm>}):}
|
||||
\begin{itemize}[<+->]
|
||||
\item \cppcmd{std::for_each}
|
||||
\item \cppcmd{std::transform}
|
||||
\end{itemize}
|
||||
\uncover<+->{But they are quite different.}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}[fragile]{Maps (2)}
|
||||
\uncover<+->{\cppcmd{std::for_each} applies a function \cppcmd{void fun(T &a)} to elements
|
||||
of an iterator containing values of type \cppcmd{T} in place:}
|
||||
|
||||
\begin{uncoverenv}<+->
|
||||
\begin{cppcode}
|
||||
vector<int> a{1,2,3};
|
||||
for_each(a.begin(), a.end(),
|
||||
[](int &n){ n*=2; });
|
||||
\end{cppcode}
|
||||
\end{uncoverenv}
|
||||
|
||||
\uncover<+->{This multiplies each element in \cppcmd{a} by 2.}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}[fragile]{Maps (3)}
|
||||
\uncover<+->{In contrast, \cppcmd{std::transform} returns a new iterator containing type
|
||||
\cppcmd{U}. Thus, the function has to be \cppcmd{U func(T val)}:}
|
||||
|
||||
\begin{uncoverenv}<+->
|
||||
\begin{cppcode}
|
||||
vector<int> b{1,2,3,4};
|
||||
vector<double> c(b.size(), 0.0);
|
||||
transform(b.begin(), b.end(), c.begin(),
|
||||
[](int i){ return 1.0/i; });
|
||||
\end{cppcode}
|
||||
\end{uncoverenv}
|
||||
|
||||
\uncover<+->{This gives a vector c filled with the inverse elements of b.}
|
||||
\uncover<+->{There are also forms of \cppcmd{transform} that merge two
|
||||
iterators (see examples in git-repo).}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}[fragile]{Filters}
|
||||
\begin{itemize}[<+->]
|
||||
\item This is ugly
|
||||
\item No syntactic sugar as with python's list comprehensions
|
||||
\item Use \cppcmd{std::remove_if} or \cppcmd{std::remove_copy_if} from \cppcmd{<algorithm>},
|
||||
\item afterwards \cppcmd{transform}.
|
||||
\item Or make use of the \cppcmd{boost::filter_iterator} library.
|
||||
\end{itemize}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}[fragile]{Folds}
|
||||
\begin{itemize}[<+->]
|
||||
\item \cppcmd{std::accumulate} is defined in \cppcmd{<numeric>}
|
||||
\item Takes bounds of an Iterator, and a \cppcmd{BinaryOperation}
|
||||
\item Example:
|
||||
\begin{cppcode}
|
||||
vector<int> a{1,2,3,4}
|
||||
int b = accumulate(a.begin(), a.end(), 0); // sum
|
||||
int c = accumulate(a.begin(), a.end(), 15, minus<int>());
|
||||
\end{cppcode}
|
||||
\cppcmd{std::minus<int>} is defined in \cppcmd{<numeric>} as well.
|
||||
\end{itemize}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}[fragile]{Generics and \texttt{D}}
|
||||
\begin{itemize}[<+->]
|
||||
\item These are only procedural examples of functional programming.
|
||||
\item Much can be done using \emph{generic} techniques
|
||||
(\enquote{templates}).
|
||||
\item Many examples: \cite{generics}
|
||||
\item Much more to come in \cpp{}20/22 (\cite[What will Not make it into
|
||||
C+17,…]{cpp17})
|
||||
\begin{itemize}[<+->]
|
||||
\item \emph{Concepts} are kind of constraints on template parameters.
|
||||
\item \emph{Ranges} will replace iterators
|
||||
\item All of the above and more are available in the \texttt{D}
|
||||
programming language! (\url{dlang.org})
|
||||
\end{itemize}
|
||||
\end{itemize}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}[fragile]{Generics example: Folds}
|
||||
\begin{uncoverenv}<+->
|
||||
Using \cpp{}11 with variadic templates, one has (mod \cppcmd{auto})
|
||||
\begin{cppcode}
|
||||
auto sum() { return 0; }
|
||||
|
||||
template<typename T>
|
||||
auto sum(T t) { return t; }
|
||||
|
||||
template<typename T, typename... Ts>
|
||||
auto sum(T t, Ts... ts) { return t + sum(ts...); }
|
||||
\end{cppcode}
|
||||
\end{uncoverenv}
|
||||
\begin{uncoverenv}<+->
|
||||
With new folding expression (\cite{cppfolds}):
|
||||
\begin{cppcode}
|
||||
template<typename T>
|
||||
auto sum(const auto&... args)
|
||||
{ return (args + ...); }
|
||||
\end{cppcode}
|
||||
\end{uncoverenv}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}[plain]{References}
|
||||
\printbibliography
|
||||
\end{frame}
|
||||
|
||||
\section{The}
|
||||
\subsection{end}
|
||||
|
||||
\begin{frame}[plain]{Thanks for listening!}{Any questions?}
|
||||
\href{https://git.f3l.de/pheerai/wtfunctional/}{Git-Repo with examples and
|
||||
slides}: \url{git.f3l.de/pheerai/wtfunctional/}
|
||||
|
||||
\begin{description}
|
||||
\item[Mail:] \url{oli_r@fg4f.de}
|
||||
\item[XMPP:] \url{pheerai@im.f3l.de}
|
||||
\item[Github:] \href{https://github.com/pheerai/}{pheerai}
|
||||
\item[Misc:] Signal, Telegram,…
|
||||
\item[…or] later having some drink
|
||||
\end{description}
|
||||
\vfill
|
||||
\tiny \raggedleft Proudly typed using Lua\LaTeX{}. Slides-theme: \emph{Antibes}\\
|
||||
Fonts used are \href{github.com/mozilla/Fira}{\emph{Fira Sans}} and
|
||||
\href{leonardo-m.livejournal.com/77079.html}{\emph{Inconsolata G}}.\\
|
||||
Syntax and code highlighting with
|
||||
\href{https://github.com/gpoore/minted}{\emph{minted}} and
|
||||
\href{http://pygments.org}{\emph{pygments}}.
|
||||
\end{frame}
|
||||
|
||||
\end{document}
|
||||
|
||||
%%% Local Variables:
|
||||
|
|
Loading…
Reference in a new issue