1 基本使用

1.1 介绍

Julia程序语言是一种计算机编程语言, 就像C、C++、Fortran、Java、R、Python、Matlab等程序语言一样。 Julia语言历史比较短,发布于2012年, 是MIT的几位作者(Jeff Bezanson, Stefan Karpinski, Viral Shah, Alan Edelman)和全世界的参与者共同制作的。 主网站在https://julialang.org/

Julia与R、Python等一样是动态类型语言, 程序与R、Python一样简单, 但是它先进的设计使得Julia程序的效率基本达到和C、Fortran等强类型语言同样的高效率。 尤其适用于数值计算,在现今的大数据应用中也是特别合适的语言, 排在Python、R之后,已经获得广泛的关注, 现在用户较少只是因为历史还太短。

本文介绍Julia语言的软件安装设置, 基本的计算编程。 目标读者是做数值计算、统计分析等工作的实际用户而不是开发人员, 作者假定读者具有一定的编程语言基础, 比如,学过R、Python、Matlab、C、Java等编程语言。 本文作者后续还计划编写关于统计数据整理、作图、统计分析的入门教材。

注意, Julia使用即时编译技术, 用LLVM虚拟机执行, 这使得其程序在初次运行时好像与R、Python这些动态语言相比响应很慢, 这其实是在进行即时编译, 用编译的等待换取运行时的高效率, 对于不能向量化的程序, Julia可以比R、Python这些动态语言快一个数量级。

Julia不同版本的程序用法有细微差别, 本文例子基于Julia v1.7和中文Windows 10操作系统。

1.1.1 学习资源

  • Julia官方文档: https://docs.julialang.org/en/v1/

  • Julia扩展包查找: https://juliahub.com/ui/Packages

  • 清华大学开源软件镜像站: https://mirror.tuna.tsinghua.edu.cn/help/julia/

  • Lauwens, Ben and Downey, Allen. Think Julia How to Think Like a Computer Scientist. O’Reilly, 2018. https://benlauwens.github.io/ThinkJulia.jl/latest/book.html

  • Storopoli, Jose, Huijzer, Rik and Alonso, Lazaro. Julia Data Science. 2021.

    主要内容是数据框、Makie作图。

  • Boyd, Stephen and Vandenberghe, Lieven. Introduction to Applied Linear Algebra; Introduction to Applied Linear Algebra Julia Companion. Cambridge University Press, 2018.

    用Julia辅助讲授线性代数。

  • Kochenderfer Mykel J. and Wheeler, Tim A. Algorithms for Optimization. MIT Press, 2019.

    用Julia表示最优化算法。

  • Alan Edelman, David P. Sanders and Charles E. Leiserson. Introduction to Computational Thinking. MIT. https://computationalthinking.mit.edu/Spring21/

    用Pluto图形界面讲解计算机编程、交互图形与数学、应用科学的结合。

  • McNicholas, Paul D. and Tait, Peter. Data Science with Julia. Chapman and Hall/CRC, 2019.

    给了机器学习的一些具体程序实现, 如最近邻,决策树,bootstrap, 随机森林,梯度法, 主成分分析,EM算法,k均值聚类,混合模型估计。

  • Sengupta, Avik. Julia High Performance. Packt, 2016.

    程序效率问题。

  • Balbaert, Ivo. Getting Started with Julia Programming. Packt, 2015.

  • Joshi, Anshul. Julia for Data Science. Packt, 2016.

    讲了数据整理, 统计分布,决策树,朴素贝叶斯,k均值聚类, 装袋法,提升法,随机森林,时间序列分解,ARIMA, 推荐系统,深度学习介绍。

  • Sherrington, Malcolm. Mastering Julia. Packt, 2015.

  • Ben Lauwens and Allen B. Downey. 《Julia语言编程入门》(Think Julia), 中国电力出版社, 2019. 肖斌、王磊等翻译.

  • Vougaris, Z. 《Julia数据科学应用》. 人民邮电出版社, 2017.

1.2 Julia软件安装

Julia 现在的版本是1.7.3版, 虽然已经完全可以用来执行生产级的任务, 但是毕竟历史还太短, 软件的安装设置还比较复杂, 为了支持使用Jupyter笔记本运行Julia还应该安装Anaconda 3软件。 清华tuna镜像网站使得Julia的安装设置变得比较容易成功了。

初学者可以仅下载Julia的命令行程序, 待熟悉一些后再选择安装下面的某一个图形界面程序。

1.2.1 Julia命令行程序的安装和使用

从清华tuna镜像网站https://mirror.tuna.tsinghua.edu.cn/help/julia/下载最新版安装程序, 如julia-1.7.3-win64.exe,运行安装程序, 会安装在用户主目录中, 在Windows下是“C:\Users\user”, 其中user是自己在操作系统中的用户名, 安装位置如C:\Users\Lenovo\AppData\Local\Programs\Julia-1.7.3。 也会在用户主目录中生成一个.julia文件夹用于存放设置和附加软件。

Julia命令行(REPL)的运行方式是在一个字符型窗口中, 在提示行julia>后面键入命令, 回车后在下面显示结果。

Julia依赖于许多扩展包, 这些包如果从国外的主服务器下载则很慢, 经常会超时出错。 可以将下载路径指向清华tuna镜像, 办法是建立C:\Users\user\.julia\config\startup.jl文件 (其中user要替换成自己的用户名), 内容为:

# ~/.julia/config/startup.jl
ENV["JULIA_PKG_SERVER"] = "https://mirrors.tuna.tsinghua.edu.cn/julia"

在Julia命令行程序中运行如下命令可以检查镜像是否生效:

julia> versioninfo()

(其中julia>是自动显示的提示)。

在MS Windows操作系统中, 设存放Julia源程序和数据文件的目录为C:\work, 将安装后显示在桌面上的Julia图标复制到目录中, 从右键选“属性”,将“起始位置”栏改为空白。 这样读写数据和程序文件的默认位置就是图标所在的目录, 而不是操作系统中的用户主目录。

在Julia命令行, 为了显示当前工作路径, 用命令

julia> pwd()

为了将当前工作目录设置到C:\work,命令如

julia> cd("C:/work")

注意路径分隔符用/而不是\。也可以写成"C:\\work"

Julia源程序用.jl作为扩展名, 为了运行当前目录中的“myprog.jl”文件, 在命令行用命令

julia> include("myprog.jl")

在命令行状态下, 按Ctrl+C键可以中止当前正在运行的耗时较长的程序。 在行首按退格键(backspace)可以退出包管理、帮助等子命令行。 按Ctrl+D键或者运行exit()命令可以退出REPL界面。 可以用上下光标键调回以前输入过的命令, 重新运行或者修改后再运行。

仅使用命令行还不够方便, 推荐使用如下两种功能更强的集成环境:

  • Jupyter笔记本环境;
  • Visual Studio Code集成编辑环境。

1.2.2 安装Anaconda3软件

Julia一种比较友好的使用方法是借助于Jupyter笔记本软件, 这需要先安装Anaconda3套装。

从清华tuna镜像网站https://mirror.tuna.tsinghua.edu.cn/help/anaconda/下载Anaconda3最新版安装程序如Anaconda3-5.3.1-x86_64.exe并安装, 一般为单个用户安装,设用户名为user, 则会安装在C:\Users\user\Anaconda3。 会在操作系统开始按钮中建立一个Anaconda(64 bit)程序组启动, 不要添加到系统搜索路径中。 在anaconda命令行和Jupyter运行时默认根目录是C:\Users\user

设置从tuna镜像更新anaconda组成部分:

conda config --set show_channel_urls yes

这会在C:\Users\user(其中user要替换成自己的用户名)生成.condarc文件,然后修改其内容为:

channels:
  - defaults
show_channel_urls: true
default_channels:
  - https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main
  - https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/r
  - https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/msys2
custom_channels:
  conda-forge: https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud
  msys2: https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud
  bioconda: https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud
  menpo: https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud
  pytorch: https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud
  simpleitk: https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud

1.2.3 Jupyter笔记本软件的安装和使用

1.2.3.1 安装

Jupyter笔记本是一种将程序代码、说明文字、执行结果结合在一起的文档格式, 这使得用户很容易地编写带有程序结果的研究报告。 这种功能的实现由Python编写的网络后台应用程序(jupyter.exe程序)、Julia(或Python)后台执行程序(称为核心,kernel)和浏览器前端显示组成。 保存的内容是扩展名为.ipynb的文本格式的文件, 所以很容易地交换, 也支持版本控制。 核心可以是Python、Julia、R等语言。 在Quarto软件的支持下, 可以将笔记本内容转换为HTML、MS Word、PDF、网站、幻灯片、图书等格式, 并且支持数学公式、编号、引用、文献引用等功能。

JupyterLab是Jupyter的一个升级版本, 和Jupyter共用一个服务器后端。

Anaconda3是Python的开发环境和软件管理环境, 可以为不同的软件组合与版本设置不同的“环境”, 使得不同版本的软件可以无冲突地共存于同一台电脑, 其中的一个组成部分是Jupyter软件。

基于Anaconda 3的Jupyter软件安装方法如下:

在安装Julia软件之前先安装Anaconda3, 从Anaconda3的程序组中启动Jupyter笔记本软件, 就已经可以支持Python语言的笔记本。

然后安装Julia的命令行程序。

为了在Julia中安装对已有的Anaconda3中的Jupyter软件的支持, 在anaconda命令行用如下where命令找到python和jupyter软件的安装位置:

>where jupyter
C:\Users\user\Anaconda3\Scripts\jupyter.exe
>where python
C:\Users\user\Anaconda3\python.exe
C:\Users\user\AppData\Local\Microsoft\WindowsApps\python.exe

其中user是用户在操作系统中的用户名。

在Julia中,先设置Python和Jupyter的位置,然后安装和构建IJulia:

ENV["PYTHON"] = "C:\\Users\\user\\Anaconda3\\python.exe"
ENV["JUPYTER"] = "C:\\Users\\user\\Anaconda3\\Scripts\\jupyter.exe"
import Pkg
Pkg.add("IJulia")

如果中间出错,可以运行

Pkg.build("IJulia")`。

安装好IJulia以后, 可以从操作系统启动按钮的Anaconda3程序组中启动Jupyter, Jupyter会自动显示在系统默认网络浏览器中(最好是Chrome浏览器)。

如果将浏览器地址栏中的.../tree改成.../lab,还可以进入JupyterLab。

JupyterLab是Jupyter的改进版本, 功能更强,与Jupyter使用同一个服务器后端, 从Anaconda3命令行启动的命令为:

jupyter lab

启动Jupyter后,Julia引擎在Jupyter中已经可以用了, 用“File – New Notebook – Julia 1.7.3”可以新建一个支持Julia程序在其中运行的笔记本。

Anaconda中的Jupyter笔记本是默认以用户个人主目录为工作目录的, 建议就使用默认设置。 如果希望将工作目录设置为其它目录如“d:\work”, 可以将Jupyter笔记本快捷图标复制到桌面, 右键单击Jupyter笔记本快捷图标, 将“目标”部分的的“jupyter-notebook-script.py”和“%USERPROFILE%”之间插入要用的目录如“d:\work”, 并将“起始位置”也改成这个目录。 这样再从快捷图标启动, 看到的目录就是自己指定的目录了。

1.2.3.2 Jupyter笔记本用法

在笔记本中可以运行程序, 并将程序的文字、表格、图形结果以富文本形式直接和其它说明文字、程序代码一起显示在浏览器窗口, 并可以转换为HTML、Markdown、LaTeX、PDF等格式。 目前不支持转为MS Office格式。

打开Jupyter程序后会自动调用一个浏览器窗口, 其中显示了用户目录。用“新建”功能可以生成Julia的笔记本, 也可以生成Python笔记本。 实际上,Jupyter程序原来主要是针对Python笔记本。 从文件资源管理器将笔记本文件拖入这个窗口可以将笔记本纳入Jupyter的管理。

新建或编辑旧有笔记本时,在浏览器的网页内会显示菜单(File, Edit, View, Inser, Cell等)和快捷图标栏(复制粘贴等)。

笔记本的内容由“单元”组成。 笔记本单元分为Markdown和程序两种单元。 Markdown单元是文档的说明部分,采用markdown语法。 可以输入类似LaTeX格式的数学公式, 其显示由MathJax程序库支持。 程序是Julia程序。

单元工作状态分为“编辑”和“命令”两种状态。 编辑状态下单元左边框为绿色, 命令状态下单元左边框为蓝色。 在命令状态下除了菜单和快捷图标以外还可以用许多快捷键。

对于程序单元, 用鼠标单击左边编号部分可以选中单元并进入命令状态, 点击程序输入框可以选中该单元并进入编辑状态, 光标焦点在该单元时按ENTER键也可以进入编辑状态。 单元输入完成后, 用Shift+Enter将程序单元执行并在单元下方显示程序的结果。 因为仅显示单元中最后一个表达式结果, 可以在单元中用@show expr;显示表达式expr的简化形式的结果。

Markdown单元也有编辑状态和命令状态, 可以显示为原始文字格式或者富文本格式, 从原始文本格式用Shift+Enter就可以将Markdown单元显示为富文本格式。 如果Markdown单元显示为原始文字格式, 单击其输入框就可以选中该单元并进入编辑状态, 单击其输入框左边的空白可以选中该单元并进入命令状态; 如果该单元已经显示为富文本状态, 需要双击才能选中该单元并进入编辑状态, 单击可以选中该单元并进入命令状态。

如果已选中单元, 在命令状态下按回车键可以进入编辑状态。 按Shift+ENTER键可以将文字转换为富文本格式。 在编辑状态按Esc键可以进入命令状态。

缺省的单元类型是程序单元。 为了将程序单元修改为Markdown单元, 在命令状态下按m键可以将其切换为Markdown单元。 在编辑状态下先按Esc再按m键即可切换为Markdown单元。 从命令进入编辑状态只要按Enter键。

在命令状态下, 用b键在当前单元下方插入一个新单元, 用a键在当前单元上方插入一个新单元。 选中一个单元后按Shift+m可以将其与下一个单元合并。

Markdown标题如果单独占一个单元, 可以称这样的单元为标题单元, 在命令状态下用快捷键1,2,3,4,5,6可以将单元转换为一级到六级标题的单元。

Markdown单元支持LaTeX格式的数学公式, 如行内公式\(e^x=\sum_{j=0}^\infty \frac{1}{j!} x^j\), 独立公式 \[\begin{aligned} x =& 10 \times (2 + 3) \\ =& 10 \times 5 = 50 \end{aligned}\] 这些公式在HTML输出中是基于MathJax库显示的。

笔记本的“Cell–Run All”菜单可以将整个笔记本的所有程序都依次运行, 并将Markdown单元都变成富文本。

在Julia程序版本升级以后, 基于老版本的笔记本可以用“Kernel – Change kernel”菜单升级到使用新的Julia引擎。

1.2.3.3 Jupyter笔记本格式转换

Jupyter笔记本的File菜单提供了“Download as”功能, 可以下载为HTML、PDF、md、.jl源文件等格式。 其中下载为HTML和md的功能是好用的。 但是, 如果想将笔记本内容作成科技论文、图书的格式就比较困难。

Quarto是一种基于Markdown格式的科技内容写作格式, 基本格式与Jupyter的markdown单元格格式兼容, 但支持公式、公式编号、图标编号、交叉引用、文献引用这些科技写作必不可少的功能。 可以从网站https://quarto.org/下载安装。 安装好以后, 打开操作系统的一个命令行窗口, 在Jupyter文件中可以用pwd()命令找到当前文件所在的文件夹, 在命令行窗口用cd命令进入该文件夹, 设文件名为jqdemo.ipynb, 在命令行中用如下命令生成HTML结果:

quarto render jqdemo.ipynb --to html

将其中的html替换成docx, 就可以转换为MS Word格式。

如果要转换为PDf, 就比较复杂一些。 首先需要安装TinyTex软件, 这个软件将LaTeX源程序转换为PDF。 从网站https://yihui.org/tinytex/安装所用的操作系统对应的版本。 对Julia程序的Jupyter笔记本, 需要在文件开头插入一个“Raw”格式单元格, 内容如:

---
toc: true
number-sections: true
format:
  pdf:
    documentclass: book
    include-in-header: preamble.tex
---

其中preamble.tex是与笔记本文件放在同一文件夹中的文件, 内容为:

\usepackage{ctex}
\usepackage{amsthm,mathrsfs}

笔记本开头插入的toc: true表示需要有目录, number-sections: true表示章节编号, 这两个选项对HTML、Word、PDF输出都有意义。

这些安装设置做好以后, 在操作系统命令行中用如下命令将笔记本文件转换为PDF格式:

quarto render jqdemo.ipynb --to pdf

更多用法参考Quarto网站

1.2.4 安装设置Visual Studio Code的Julia语言集成编辑环境

1.2.4.1 安装

Visual Studio Code(简称VSC或VSCode)是Microsoft提供的一款免费的集成编程环境, 直接支持JavaScript, TypeScript 和 Node.js语言, 并可在插件帮助下支持常见的编程语言如C++、Python, 安装插件后可以支持Julia语言的编辑、运行。

https://code.visualstudio.com/网站下载所用操作系统对应的VSCode版本并安装。 然后进入https://marketplace.visualstudio.com/items?itemName=julialang.language-julia网站, 点击“Install”按钮, 提示打开Visual Studio Code, 进入VSCode后在Julia支持界面按“Install”按钮就可以安装好VSCode的Julia语言支持。 如果有多个Julia版本, 在VSCode的Julia语言支持的设置(齿轮按钮)界面, 可以设置Julia可执行程序路径, 如“C:\Users\Lenovo\AppData\Local\Programs\Julia-1.7.3\bin\julia.exe”。

1.2.4.2 VSCode使用方法

选择打开一个文件夹可以进入子集保存Julia源程序和数据的文件夹, 打开任意一个Julia源程序文件可以编辑和运行。

用Ctrl+Enter运行当前行、当前选择或当前结构。 用Shift+Enter运行当前行、当前选择或当前结构并将光标移动到执行的程序的下方。 程序和结果将会自动显示到一个Julia命令行窗格。

VSCode不会自动打开Julia的命令行窗格, 只有一个terminal窗格。 Windows下快捷键“Alt+J Alt+O”可以打开一个功能增强的命令行窗格(REPL)。

为了访问与Julia有关的命令, 可以选菜单“View——Command Palette”打开命令窗格, 然后搜索julia, 就显示与Juila有关的命令。

使用手册:

1.2.5 安装设置Atom+Juno集成编辑环境

1.2.5.1 安装设置

Atom是一个程序编辑器, 功能强大, 配合其Juno包可以作为Julia的一个强大的集成编辑环境(IDE)。 但是, Juno包的开发团队已经加入了VSCode集成环境的Julia支持团队, 所以Atom的Juno功能不再进一步开发。 可考虑改用VSCode。

从Atom网站https://atom.io/下载安装Atom软件。 安装后运行Atom, 选择“File–Settings”,找到“+install”选项, 在搜索框中输入“uber-juno”, 选择安装此包。 安装好以后, 再次进入Atom, 会自动下载安装与Julia相联系所需要的扩展包, 包括Atom的julia-client和julia-language包, Julia的juno包和Atom包。

因为需要从网上下载安装额外的包, 如果下载安装出错, Julia不能在Atom中运行, 可以在自己的主目录如“C:\用户\自己的用户名”中找到.atom\packages, 删除其中的uber-juno,julia-client, julia-language, 然后重新在Atom中安装其uber-juno包。 或干脆卸载Atom后重新安装Atom和Atom的uber-juno包。

安装好Atom和uber-juno后, 应该在Atom的设置中找到julia-client包, 将其中的Julia可执行程序指向新安装的Julia可执行程序julia.exe的位置, 如C:\Users\Lenovo\AppData\Local\Programs\Julia-1.7.3\bin\julia.exe。 也可以将该路径加入系统的PATH环境变量, 这样就不需要在Atom中设置Julia可执行程序的位置。

1.2.5.2 用法简介

在Atom+Juno中, 可以打开一个文件夹作为项目目录, 并选择菜单“Juno – Working Directory – Select Project Folder”, 将项目目录作为当前工作目录。

编辑“.jl”源文件, 在源文件中用Ctrl+Enter可以运行当前行或当前结构或选定行。 可以用Shift+Enter运行当前行或当前结构或选定行, 并将光标移到当前结构后面。 运行结果会以折叠方式显示在当前结构的右方。

用菜单“Juno – Run All”运行整个源文件。 注意这样运行时表达式的值不自动显示。

Atom+Juno中有一个Console窗格,即内嵌的Julia命令行。 如果没有这个窗格,可以用“Juno – Open REPL”菜单建立。 在Console窗格按ENTER键可以在此窗格中启动Julia命令行。 可以用“Juno – Stop Julia”退出这里的命令行, 然后再按ENTER键可以重启Julia命令行。

程序运行的图形结果可以显示在Atom内的一个窗格中。 Plots包可以将结果显示到Atom的图形窗格内, 建议使用GR后端或者PyPlot后端。

Atom+Juno还有一个Workspace窗格, 可以显示当前定义的变量、函数的类型和内容。 如果没有这个窗格可以用“Juno – Open Workspace”菜单建立。

Atom+Juno支持程序调试, 可以设置断点, 跟踪运行, 跟踪运行时自动显示当前的变量列表和变量值。

详见:

1.2.6 Pluto笔记本安装使用

1.2.6.1 安装

Pluto是完全用Julia语言编写的一个类似于Jupyter的笔记本软件。 安装只要在Julia中安装Pluto扩展包:

using Pkg; Pkg.add("Pluto")

参考:

1.2.6.2 用法简介

安装Pluto后, 只要在Julia的命令行中运行:

using Pluto; Pluto.run()

就可以在系统默认浏览器中打开Pluto界面。 可以用样例学习其使用方法。 为了关闭Pluto, 在浏览器中退回到初始界面后, 可以在Julia命令行用“Ctrl+C”快捷键退出Pluto。

Pluto的功能没有Jupyter那么强大, 其中的单元格默认为程序, 单元格也可以是Markdown内容, 但是需要写成:

md"""
Markdown内容
"""

这样的格式。 在markdown内容中也支持LaTeX数学公式, 但不允许在公式的头、尾有空格, 所以独立公式写法如:

$$\begin{aligned}
  & y = f(x) 
\end{aligned}$$

为了添加一个单元格, 只要点击单元格之间的左侧的“+”图标, 或者在前一单元格中使用组合键“Ctrl+ENTER”。

在单元格中用“Shift+ENTER”或者点击单元格右下角的“播放”图标可以运行修改后的程序, 或将markdown内容转换为HTML富文本显示格式。 如果使用“Ctrl+ENTER”,则运行当前单元格后还会在下面添加新单元格。

单元格左侧的“眼睛”图标可用于隐藏或显示源代码。

拖动单元格左侧栏可以移动单元格位置。

鼠标停留在某个函数上时, 可以用屏幕右下角的“Live docs”图标查找帮助; 如果是定义在本笔记本内部的函数, 可以用快捷键“Ctr点击”跳到函数定义位置。

快捷键“Ctrl+S”保存当前笔记本。 在笔记本上端有保存或者导出为其它格式的功能, 保存的文件是“.jl”的,可以直接作为Julia源程序运行, 可以导出为网页、PDF等格式。 笔记本内容左上侧的“Pluto”图标用于返回到开始界面。

为了增加自动的右边栏目录, 在笔记本开头增加如下的单元格:

PlutoUI.TableOfContents(aside=true)

Pluto的特点是支持交互, 每次修改了一个变量的值, 依赖于此变量的所有代码都自动重新运行并显示新的结果。 为了支持交互,不允许修改其它单元格的变量值。

程序单元格一般都是单条命令, 为了将多条命令放在同一个单元格需要使用begin...end结构, 或者let...end结构。

为了显示某个变量或表达式的值, 不要使用println, show, display等函数, 而是将其作为一个单元格或者作为单元格中最后一个表达式。

可以很容易地定义用来修改变量值的滑块、数值加减框等图形元素, 修改其值也会使得程序重新运行。

Pluto比较适合交互测试不同输入、不同参数的运行效果, 可以用来教学。 Pluto的每个笔记本都有自己单独的调用库, 在国内的网络环境因为重新下载库的问题有时响应很慢。 不适合中大规模的编程计算问题。

麻省理工学院提供了一套“计算思维”免费公开课, 其中的课件和作业使用了Pluto, 可以作为Pluto和Julia语言的学习资料:

1.2.7 关于JuliaPro套装

JuliaPro是Julia Computing公司(http://juliacomputing.com)的Julia集成环境, 从其网站下载JuliaPro的免费个人版, 其中包含了命令行程序, 以及基于Atom+Juno编辑器的集成编辑和运行环境。

安装后, 运行JuliaPro, 可以进入一个编辑、运行Julia程序的集成环境(IDE)。

我在安装了JuliaPro1.4.0版本后, 发现它更新包需要登录, 可以用github账户, 但是因为优先从公司网站下载, 经常遇到网络拥堵。 而且与单独安装的Anaconda3中的Python和Jupyter软件的兼容性也不好, 就卸载了JuliaPro。 个人不推荐使用。

1.3 扩展包

Julia的基本语言比较精炼, 许多功能都需要依赖扩展包(packages)完成, 一些基本的语言功能也放到了Base扩展包中, Base扩展包中的函数都是直接可以使用的, 不需要单独安装和声明。

其它的一些功能则需要从网上安装, 并且使用时需要声明。

为了调用某个扩展包的功能, 一般只要用using关键字将其提供的函数和全局变量调入到当前名字空间即可。 如

using DataFrames

Julia语言的扩展包在需要时从网上安装, 公布的扩展包都放在Github网站, 列表见:

国内现在连接Github网站不太顺畅, 应该使用前面讲过的设置初始运行文件的方法将下载扩展包的服务器指定为清华tuna镜像服务器。

在Julia命令行安装扩展包,如DataFrames,命令如

using Pkg
Pkg.add("DataFrames")

如果要安装的扩展包依赖于其它的扩展包, 这些扩展包也会自动被安装。

安装的扩展包如果不能正常工作, 可以尝试重新构建,如:

using Pkg
Pkg.build("DataFrames")

为了更新已安装的所有包到最新版本, 用命令

Pkg.update()

在REPL命令行按“]”键可以进入包管理环境, 可以运行一些扩展包管理命令。

  • help命令:查看扩展包管理命令。
  • status命令:查看安装的扩展包列表。
  • add xxx命令:安装某个包。
  • build xxx命令:使得包能够运行。
  • update xxx命令:更新包到新版本。
  • remove xxx命令:删除包。

1.4 Julia的基本数据和相应运算

1.4.1 整数与浮点数

Julia程序中的整数值可以直接写成如123或者-123这样。 虽然整数有多种类型, 一般程序中不必特别关心整数常量的具体类型。 Julia允许使用特别长的整数,这时其类型为BigInt。 较长的整数常数可以写成如123_456_789这种格式。

Julia的浮点数可以写成带点的形式如123.0, 1.23, 也可以写成带有10的幂次如1.23e3(表示\(1.23\times 10^3\)), 1.23e-3(表示\(1.23\times 10^{-3}\))。 这些写法都属于Float64类型的浮点数。 Julia还有其他类型的浮点数,但是科学计算中主要使用Float64类型, 在别的语言中这称为双精度浮点数。

Julia还提供了任意精度整数与任意精度浮点数。

布尔类型Bool只有两个值:true和false。

typeof()求某个值的类型,如:

typeof(123)
## Int64
typeof(1.23)
## Float64
typeof("Julia")
## String

1.4.2 四则运算

表示加、减、乘、除、乘方的运算符分别为:

+  -   *  /  ^

浮点数的四则运算遵循传统的算数运算规则和优先级规定。 用圆括号改变优先级。如

(1.3 + 2.5)*2.0 - 3.6/1.2 + 1.2^2
## 6.039999999999999

表示 \[(1.3 + 2.5) \times 2.0 - 3.6 \div 1.2 + 1.2^2\] 注意浮点运算引起会造成数值计算误差。

在交互运行时,特殊变量ans表示最后一个计算过的表达式的值。如:

ans
## 6.039999999999999

1.4.3 整数的四则运算

整数加法、减法、乘法结果仍为整数, 这样因为整数的表示范围有限,有可能发生溢出, 而且溢出时Julia并不出错而且也不提示。

例:

10 + 2*3 - 3*4
## 4

整数用“/”作的除法总是返回浮点数,即使结果是整数也是一样:

10/2
## 5.0

求整数除法的商,用÷运算符, 如

5 ÷ 3
## 1

其中÷的输入方法是在命令行中输入\div后按TAB键。 这种方法可以输入许多数学符号, 如α(alpha),π(pi),(sum),等等。

整数用a % b表示a整除b的余数,结果符号总是取a的符号。 也可以写成rem(a, b)。 如

10 % 3
## 1

divrem(x, y)同时返回x除以y的商和余数, 如:

divrem(10, 3)
## (3, 1)

整数与浮点数的混合运算会将整数转换成浮点数再计算。

1.4.4 数学函数

和其它科学计算语言类似, Julia也支持常见的数学函数, 如log, exp, sqrt, sin, cos, tan等。

round(Int, x)x四舍五入为整数, round(x, digits=2)x四舍五入到两位小数, round(x)x四舍五入到0位小数的浮点数。 floor(Int, x)求小于等于x的最大整数, floor(x)返回浮点数结果, ceil(Int, x)求大于等于x的最小整数, ceil(x)返回浮点数结果。

factorial(n)计算阶乘, binomial(n, k)计算nk的组合数。 gcd(x,y)计算最大公因数, lcm(x,y)计算最小公倍数。 ndigits()求数值以某个进制表示后的数字位数(仅数字)。 evalpoly()计算多项式。

1.4.5 字符串

单个字符在两边用单撇号界定,如'A''囧'。 字符都是用Unicode编码存储,具体使用UTF-8编码。 每个字符可能使用1到4个字节表示。 字符的类型为Char, 自定义函数中的字符参数可声明为AbstractChar。

零到多个字符组成字符串, 程序中的字符串在两边用双撇号界定,如 "A cat""泰囧"。 字符串数据类型名称为String, 自定义函数中的字符串参数可声明为AbstractString。

对于占据多行的字符串, 可以在两侧分别用三个双撇号界定。如

"""
这是第一行
这是第二行
三个双撇号界定的字符串中间的单个双撇号"不需要转义
"""
## "这是第一行\n这是第二行\n三个双撇号界定的字符串中间的单个双撇号\"不需要转义\n"

注意多行内容的首行不需要紧挨着写在开头的三个双撇号后面同一行内。

字符串属于不可修改类型(immutable), 即不能直接修改字符串的内容, 但可以给保存了字符串的变量赋值为一个新的字符串。

用星号“*”连接两个字符串,也可以将字符连接成字符串,如

'#' * "这是" * "美好的一天" * "。"
## "#这是美好的一天。"

用“^”表示字符串重复,如:

"--+" ^ 3
## "--+--+--+"

Julia支持输入和使用一些特殊的Unicode字符,如÷,≥, 🐢。 这些字符的输入方法是输入反斜杠和类似Latex符号名的字符串后按TAB键, 比如:

  • π \pi+TAB
  • ÷ \div+TAB
  • \ge+TAB
  • \le+TAB
  • \ne+TAB
  • \in+TAB
  • \subset+TAB
  • \supset+TAB
  • 🐢–\:turtle:+TAB

这些字符有些是作为运算符的, 比如“÷”表示除法的向下取值。

从别的Julia源文件复制得到的数学符号如果不知道怎么输入, 可以在命令行用“?符号”的形式查询,如:

> ?÷
"÷" can be typed by \div<tab>
......

字符串中可以用$变量名$(表达式)的格式插入变量或表达式的值, 称为字符串插值。例如

name="John"; "My name is $name"
## "My name is John"
"100$(name)999"
## "100John999"

1.4.6 符号(Symbol)

符号是类似于字符串的一种数据类型, 写成如:name这个样子, 符号不像字符串那样可以进行各种处理, 一般用来保存变量名、选项等。

string(s)将一个符号转换为字符串类型。 用Symbol(s)将一个字符串转换为字符串。如:

string(:name)
## "name"
Symbol("name")
## :name

1.4.7 枚举类别

某些类型仅取有限个已知的值, Julia语言用枚举类型表示这样的取值类型。 定义如:

@enum Fruit apple pear orange
my_order = orange
## orange::Fruit = 2

可以看出, 枚举类型是从0开始编号表示的。

typeof(my_order)
Enum Fruit:
apple = 0
pear = 1
orange = 2
Integer(my_order)
## 2

1.5 变量与赋值

变量名是一个标识符, 用来指向某个值在计算机内存中的存储位置。 变量名可以用英文大小写字母、下划线、数字、允许的Unicode字符, 区分大小写。 变量名不允许使用空格、句点以及其它标点符号和井号之类的特殊字符。

为兼容性起见,尽可能不要用汉字作为变量名。 变量名主要使用小写字母、数字和下划线构成, 两个英文单词之间可以直接连在一起,如totalnumber, 或者用下划线连接,如total_number, 也可以在单词之间使用开头字母大写来区分单词, 如totalNumber, 第一个单词仍全部用小写。

给变量赋值,即将变量名与一个内存中的内容联系起来,也称为绑定(binding), 使用等号“=”,等号左边写变量名,右边写要保存到变量中的值。如

x = 123
## 123
y = 1+3/2
## 2.5
addr10086 = "北京市海淀区颐和园路5号"
## "北京市海淀区颐和园路5号"

变量的类型是由它保存的(指向的内存中的)值的类型决定的, 不需要说明变量类型。 Julia允许说明变量类型,但一般不需要, 仅在编写函数时为了提高运行效率可以说明自变量和返回值类型。

变量赋值后,就可以参与运算,如:

x = 123
y = 1+3/2
x + y*2
## 128.0

在命令行运行时, 特殊变量名ans表示前一个表达式的计算结果。

变量名前面紧挨着数字表示相乘,如

x + 2y
## 128.0

x = y = z = 1这样的赋值表示z=1, y=z, x=y这样的从右向左的连续赋值。

赋值还有一种计算修改简写方式, 即将变量值进行四则运算后保存回原变量, 格式为x op= expr, 其中op是某种四则运算, 这等价于x = x op expr, 如:

x = 123
x += 100
## 223

要注意的是, 如果x是一个变量, y = x不是简单的复制x的值给变量y, 对于可变类型的x, 需要用y = copy(x)或者y = deepcopy(x)。 见“一维数组”章节。

1.6 比较和逻辑运算

1.6.1 比较运算

两个数值之间用如下的比较运算符进行比较:

==   !=   <   <=   >   >=

分别表示等于、不等于、小于、小于等于、大于、大于等于。 其中<=可以写成>=可以写成。 要特别注意“等于”比较用两个等号表示。

比较的结果是true(真值)或者false(假值)。 结果类型为布尔型(Bool)。

1 == 1.0 
## true
2 != 2.0001
## true
3.5 > -1
## true
3.5 < -1.5
## false
-3.5 >= 1.2
## false
-3.5 <= 1.2
## true

两个字符串之间也可以比较, 比较时按字典序比较, 两个字符的次序按照其Unicode编码值比较。如

"abc" == "ABC"
## false
"ab" < "abc"
## true
"陕西省" == "山西省"
## false

1.6.2 逻辑运算

比较通常有变量参与。如

age = 35; sex="F"
age < 18
## false
sex == "F"
## true

有时需要构造复合的条件, 如“年龄不足18岁且性别为女”, “年龄在18岁以上或者性别为男”等。

&&表示要求两个条件同时成立, 用||表示只要两个条件之一成立则结果为真, 用!cond表示cond的反面。 如

age < 18 && sex == "F"
## false
age >= 18 || sex == "M"
## true

&&||都是短路运算符, 对expr1 && expr2, 只要表达式expr1是假值, 就直接返回假值,不会再计算expr2。 类似地, 对expr1 || expr2, 只要表达式expr1是真值, 就直接返回真值,不会再计算expr2

1.7 简单的输出

在Julia命令行,键入变量名或者计算表达式直接在下面显示结果。 可以用println()函数显示指定的变量和结果。如

println(x + y*2)
## 228.0

println()中多个要输出的项用逗号分开。 两项之间没有默认的分隔, 如果需要分隔可以自己写在输出项中。

println("x=", x, " y=", y, " x + y*2 =", x+y*2)
## x=223 y=2.5 x + y*2 =228.0

println()函数输出会将后续输出设置到下一行, 而print()函数与println()类似但是将后续输出设置在当前行。

在命令行运行时, 表达式的值自动显示。 在表达式末尾用分号结尾表示不要显示该表达式的结果。

在用include()命令执行整个脚本文件时, 每个表达式的结果默认不自动显示。

show(expr)显示表达式expr的值, 格式可能与println(expr)不同。

@show expr用有提示的方式显示表达式expr的值, 并返回此值,如:

@show 1+2
## 1 + 2 = 3
## 3

display(expr)也显示表达式expr的值。

1.8 程序控制结构

1.8.1 注释

Julia语言用#号开始注释。注释可以单独写一行, 也可以写在程序行末尾。 注释用来说明程序员的意图, 写出自定义函数的用法, 临时取消部分命令的运行, 等等。

如:

# 这是一行注释。
function f(x)
    return x^2 + 1 # 返回值
end

Julia也支持多行注释,这时以#=开始注释,用=#结束注释。

1.8.2 复合表达式

begin ... end可以将多行的多个表达式组合起来当作一个表达式, 复合表达式的值是其中最后一个表达式的值。 如

z = begin
  x = 1
  y = 2
  x + y
end
z
## 3

多个表达式也可以用分号分隔后写在圆括号中,作为一个复合表达式,如

z = (x = 1; y = 2; x + y)
z
## 3

多个语句可以用分号分开放在同一行,如:

x = 1 + 2; println("x=$x")
## x=3

1.8.3 短路与运算以及分支结构

&& 是一种短路运算, 表达式cond && expr 仅当cond为true时才计算(运行)expr, 所以这种写法经常用作程序分支的简写: 条件cond为真时执行expr, 否则不执行。 可以理解成“当cond成立时执行expr”。

比如,在计算x的平方根之前,先判断其非负:

x = -1.44
x < 0 && println("平方根计算:自变量定义域错误,x=", x)
## 平方根计算:自变量定义域错误,x=-1.44

1.8.4 短路或运算以及分支结构

||是一种短路或运算,表达式cond || expr 仅当cond为false时才计算(运行)expr, 所以这种写法经常作为程序分支的缩写: 条件cond为假时才执行expr,否则不执行。 可以理解成“除非cond成立才不执行expr”。

比如,求平方根时当自变量不为负时才计算平方根:

x < 0 || (y = sqrt(x))
## true

1.8.5 if–end结构

可以用if cond ... end结构在条件cond成立时才执行某些语句,如

x = 1.44
if x >= 0
  y = sqrt(x)
  println("√", x, " = ", y)
end
## √1.44 = 1.2

注意条件不需要用括号包围,结构以end语句结尾。 另外,if的条件必须是布尔型结果, 而不像其它语言可以直接用非零的表达式作为真值。

1.8.6 if–else–end结构

if cond ... else ... end结构当条件成立时执行第一个分支中的语句, 当条件不成立时执行第二个分支中的语句。 如

x = -1.44
if x >= 0
  y = sqrt(x)
  println("√", x, " = ", y)
else
  y = sqrt(-x)
  println("√", x, " = ", y, "i")
end
## √-1.44 = 1.2i

1.8.7 if-elseif-else-end结构

if cond1 ... elseif cond2 ... else ... end可以有多个分支, 有多个条件cond1, cond2, ……, 依次判断各个条件,那个条件成立就执行对应分支的语句, 所有条件都不成立则执行else分支的语句。 条件cond2隐含条件cond1不成立, cond3隐含cond1cond2都不成立, 依此类推。 要注意elseif中间没有空格。

例如

age = 35
if age < 18
  println("未成年")
elseif age < 60
  println("中青年")
elseif age < 100
  println("老年")
else
  println("老寿星!")
end
## 中青年

1.8.8 三元运算符

可以用cond ? expr1 : expr2表示比较简单的两分支选择, 当cond成立时结果为expr1的结果, 当cond不成立时结果为expr2的结果。 仅计算其中一个分支的值。 如

x = -1.44
y = x >= 0 ? sqrt(x) : sqrt(-x)
## 1.2

注意:问号“?”和冒号“:”两边必须有空格。

ifelse()函数提供了类似功能, 区别在于ifelse()是正常的函数, 会计算两个分支的值, 从中选择一个作为返回值, 而不是仅仅计算一个分支:

x = -1.44
y = ifelse(x >= 0, sqrt(x), sqrt(-x))
DomainError with -1.44:
sqrt will only return a complex result if called with a complex argument. Try sqrt(Complex(x)).
.........

1.8.9 for循环

for循环一般是沿着某个范围进行计算或处理,格式如下:

for loopvar = a:b
  expr1
  expr2
  ...
end

其中loopvar是自己命名的循环变量名, for结构块内的语句(表达式)先对loopvar=a运行, 再对loopvar=a+1运行, 最后对loopvar=b运行,然后结束。 如

for i=1:3
  y = i^3
  println(i, "^3 = ", y)
end
1^3 = 1
2^3 = 8
3^3 = 27

范围也可以是1:2:9, 0:0.1:1这样的带有跨度(增量)的, 可以是倒数的如3:-1:1表示3, 2, 1。 倒数时一定要包括中间的负数值的步长, 而不能直接写成3:1这样的形式。

要小心的是,循环内的变量都是循环的局部变量, 比如上例中的iy都是局部的, 退出循环后无法访问iy的值。

如果是用include()方法运行整个源程序文件而不是在交互运行, 则循环内可以读取循环外部的全局变量值但是不能修改全局变量的值。

当循环在自定义函数内时, 可以读写循环外的变量, 但是循环变量以及循环内新定义的变量仍不能在退出循环后保留。

在自定义函数之外使用循环外的全局变量时, 如果不是在函数定义内部, 应该在循环开始处用global关键字声明该变量。 如:

n = 5
p = 1
for i = 1:n
    global p
    p *= i
end
println(p)
## 120

Julia程序在REPL、Jupyter界面等交互运行环境时设置为循环内的变量能够直接访问循环外的变量, 不需要global声明。 这种不允许循环内部修改全局变量的设置看起来不太合理, 但正规的Julia程序一般都编写为函数形式, 在函数中的for循环是可以自由地读写函数内的变量的。

1.8.10 两重for循环

for循环可以嵌套,如

for i=1:9
    for j=1:i
        print(j, "×", i, "=", i*j, " ")
    end
    println()
end
1×1=1 
1×2=2 2×2=4
1×3=3 2×3=6 3×3=9
1×4=4 2×4=8 3×4=12 4×4=16
1×5=5 2×5=10 3×5=15 4×5=20 5×5=25
1×6=6 2×6=12 3×6=18 4×6=24 5×6=30 6×6=36
1×7=7 2×7=14 3×7=21 4×7=28 5×7=35 6×7=42 7×7=49 
1×8=8 2×8=16 3×8=24 4×8=32 5×8=40 6×8=48 7×8=56 8×8=64
1×9=9 2×9=18 3×9=27 4×9=36 5×9=45 6×9=54 7×9=63 8×9=72 9×9=81

这种两重循环可以简写为一个for语句,外层循环先写, 内层循环后写,中间用逗号分隔。 如

for i=1:9, j=1:i
  print(j, "×", i, "=", i*j, " ")
  j==i && println()
end

1.8.11 while循环

for循环适用于对固定的元素或者下标的遍历, 在未预先知道具体循环次数时,需要使用当型循环循环或者直到型循环。

Julia中用while cond ... end表示当型循环, 在条件cond成立时执行结构内的语句, 直到cond不成立时不再循环。

这种循环一定要使得循环条件是在有限步内会成立的, 否则就产生无限循环,称为死循环,程序将无法终止。

与for循环类似, while循环内的变量也是局部的, 在非自定义函数内while循环访问外部变量时也要用global关键字声明。 如:

s = 1
i = 0
while i < 5
    global i, s
    i += 1
    s *= i 
end 
println(s)
## 120

1.8.11.1 例:求最大公约数

求两个整数\(m\), \(n\)的最大公约数, 可以使用辗转相除法。 例如, 210和24,计算过程为:

\[\begin{aligned} 210 \mod 24 =& 18, \\ 24 \mod 18 =& 6, \\ 18 \mod 6 =& 0 . \end{aligned}\] 最大公约数为6。

程序:

function mygcd(m, n)
  local r
  while n  0
    r = m % n 
    m, n = n, r 
  end

  return m 
end

mygcd(210, 24)
## 6

标准库中有gcd函数用来计算最大公约数。

1.8.11.2 例:平方根计算

求某个正数\(x\)的平方根, 相当于求解方程\(f(u) = u^2 - x = 0\)。 利用一阶泰勒展开式\(f(u) = f(u_0) + f'(u_0)(u - u_0) + o(u-u_0)\), 其中\(f'(u) = 2u\),可以得到迭代公式 \[ u_n = u_{n-1} - \frac{f(u_{n-1})}{f'(u_{n-1})} = u_{n-1} - \frac{u_{n-1}^2 - x}{2 u_{n-1}} = \frac12 \left( u_{n-1} + \frac{x}{u_{n-1}} \right) \] 给定一个初始值\(u_0\), 迭代直到\(|u_{n} - u_{n-1} < \epsilon\)为止, \(\epsilon\)是预先给定的精度如\(10^{-6}\)

程序如下:

function mysqrt(x, eps=1E-6)
    u = 1.0
    u1 = 0.0
    while abs(u - u1) >= eps
        u1 = u
        u = 0.5*(u + x/u)
    end
    return u
end
mysqrt(2)
## 1.414213562373095

1.8.11.3 例:级数计算

数学家Srinivasa Ramanujan提出了如下的计算\(\frac{1}{\pi}\)的级数: \[ \frac{1}{\pi} = \frac{2\sqrt{2}}{9801} \sum_{k=0}^\infty \frac{(4k)!(1103 + 26390k)}{(k!)^4 396^{4k}} \]

k = 0
x = 2*sqrt(2)/9801
y = 1103
z = x*y
s = z
while z > 1E-15
    k += 1
    k4 = 4*k
    x *= k4*(k4-1)*(k4-2)*(k4-3)/k^4/396^4
    y += 26390
    z = x*y
    s += z
end
println("k=", k, " estimate = ", s)
println("Error = ", s - 1/π)
## k=2 estimate = 0.3183098861837907
## Error = 0.0
1/π
## 0.3183098861837907

1.8.12 直到型循环与break语句

当型循环每次进入循环之前判断循环条件是否成立, 成立才进入循环。

直到型循环每次先进入循环,在循环末尾判断循环退出条件是否满足, 满足退出条件时就不再循环。 Julia语言没有提供专门的直到型循环语法,可以用如下的方法制作直到型循环:

while true
  expr1
  expr2
  ...
  cond && break
end

其中cond是循环退出条件。break语句表示退出一重循环。

例如,用泰勒展开近似计算自然对数\(\log(1 + x)\)\[ \log(1 + x) = x + \sum_{k=2}^\infty (-1)^{k-1} \frac{x^k}{k} \]

实际计算时不可能计算无穷次,所以指定一个精度如eps=0.0001, 当计算的通项小于此精度时停止计算。 程序用直到型型循环写成:

eps = 0.0001
x = 1.0
y = x; xk = x; sgn = 1; k = 1
while true
  global k, sgn, xk, y, eps
  k += 1; sgn *= -1; xk *= x
  item = xk / k
  y += sgn*item
  item < eps && break
end
println("eps = ", eps, " log(1+", x, ") = ", y, 
  " Iterations: ", k)
## eps = 0.0001 log(1+1.0) = 0.6931971730609582 Iterations: 10001

1.8.13 continue语句

break语句可以在循环内退出当前所在的循环, 去继续执行当前所在循环后面的程序。 continue语句不是退出整个循环, 而是中止当前一轮循环, 进入循环的下一轮。 在for循环和while循环中都可以使用break语句和continue语句。 如:

for i = 1:5
    println(i)
    if i==3
        continue
    end
    y = i*i 
    println(y)
end
1
1
2
4
3
4
16
5
25

1.8.14 goto命令

对于多重循环, break仅退出当前所在的循环, 可以用布尔型变量指示继续判断是否退出更多的循环。 Julia提供了@goto mylable@label mylabel这样的定义无条件转移到标签位置和定义标签的命令, 可以用来退出深层的循环。

1.9 自定义函数初步

Julia语言支持自定义函数。 在计算机程序语言中, 函数是代码模块化、代码复用的基础。 将常用的计算或者操作写成函数以后, 每次要进行这样的计算或者操作, 只要简单地调用函数即可。 将复杂的任务分解成一个个函数, 可以使得任务流程更加明晰, 任务的不同阶段之间减少互相干扰。

Julia用户需要了解的一点是, Julia利用一个叫做LLVM的即时编译(JIT)系统执行程序, Julia编译器先将程序编译为LLVM中间代码, LLVM再将中间代码转换成本地二进制代码执行, 这使得Julia程序运行效率可以与C++等强类型编译语言相比拟, 但这种做法使得Julia函数在第一次运行时产生额外的时间开销, 第二次运行时就没有额外开销了。 写成函数的Julia代码通常比没有写成函数的代码运行速度快很多, 因为函数的代码在编译时会进行许多优化, 而不写成函数的代码优化较少。

1.9.1 自定义函数的单行格式

类似于\(f(x) = x^2 + 3 x + 1\)这样的简单函数, 可以用一行代码写成

f(x) = x^2 + 3*x + 1

调用如

f(2)
## 11
f(1.1)
## 5.510000000000001

1.9.2 自定义函数的多行格式

需要用多行才能完成的计算或者操作, 就需要写成多行的形式。格式如

function funcname(x, y, z)
  ...
end

其中funcname是函数名称, x, y, z等是自变量名, ...是函数内的语句或表达式, 定义以end关键字结尾, end也是许多其它程序块的结尾标志。 函数体内的语句(表达式)一般缩进2到4个空格对齐, 但不像Python那样是必须的, 缩进主要是为了程序的可读性。 函数以最后一个表达式为返回值(结果), 但最好用return关键字指定返回值。 不需要返回值的函数可以返回特殊的nothing值。 nothing是类似R中的Null的值, 表示不存在。 Julia中用missing表示缺失值。

例如,写一个函数,以向量的形式输入一个变量的样本值, 计算样本标准差: \[ s = \sqrt{ \frac{1}{n-1} \sum_{i=1}^n (x_i - \bar x)^2 } . \]

自定义函数如

# mysd: Input numeric vector x, output its sample standard deviation.
function mysd(x)
  n = length(x)
  mx = sum(x) / n
  s = 0.0
  for z in x
    s += (z - mx)^2
  end
  sqrt(s / (n-1))
end

调用如

mysd([1, 2, 3, 4, 5])
## 1.5811388300841898

事实上,上面的函数定义可以用向量化方法进行简化,如

function mysd_simple(x)
  n = length(x)
  mx = sum(x)/n
  sqrt( sum(x .- mx) / (n-1) )
end

第二个版本x .- mx表示向量x每个元素与一个标量mx相减。 这样的做法在Julia中运行效率并不比第一个版本直接循环的效率更好, 甚至可能还不如第一个版本。 Julia语言与R、Matlab等语言不同, 显式的循环一般有更高的执行效率, 向量化写法仅仅是可以使得程序比较简洁。

1.9.3 可选参数(Optional argument)和关键词参数(Keyword arguments)

函数的某些参数可以在定义时指定缺省值, 称为可选参数, 在调用时可以省略这些参数。 比如:

f_quad(x, a=1, b=0, c=0) = a*x^2 + b*x + c
## f_quad (generic function with 4 methods)

这样,调用f_quad(x)相当于f_quad(x, 1, 0, 0), f_quad(x, 2)相当于f_quad(x, 2, 0, 0), f_quad(x, 2, 4)相当于f_quad(x, 2, 4, 0)。 调用时总是假定最后的参数取缺省值而不能令中间的某个参数取缺省值而后续的参数取指定值, 所以f_quad(x, 2, 4)相当于f_quad(x, 2, 4, 0)而不是f_quad(x, , 0, 4)。 不带缺省值的参数和带有缺省值的参数都称为按位置对应的参数(positional arguments), 参数(称为虚参)与调用时的值(称为实参)按位置次序对应, 实参个数少于虚参个数时,缺少的必须是排在最后可选参数,按缺省值调用。

注意上述函数定义显示f_quad有4个“方法”, 这与泛型函数有关, 同名的函数允许按照不同的位置参数个数、位置参数类型而有不同的“方法”, 即不同的定义。

另外一种带有缺省值的参数称为关键字参数, 在定义函数时这些参数必须写在分号的后面。 关键字参数一般是函数的一些选项, 调用函数时仅当需要使用与缺省选择不同的选项时才输入关键字参数的值, 必须使用“参数名=参数值”的格式输入实参值, 而不允许按位置对应。 调用时关键字参数前面可以用逗号, 而不是必须用分号。

例如:

function f_quad2(x, a=1, b=0, c=0; descending=true)
    if descending
        return a*x^2 + b*x + c
    else
        return c*x^2 + b*x + a
    end
end

则调用f_quad(x)相当于f_quad(x, 1, 0, 0)x^2, 调用f_quad(x, descending=false)相当于f_quad(x, 0,0,1)1。 调用时关键字参数与位置参数之间可以用分号分隔也可以用逗号分隔, 比如调用时f_quad(x, descending=false)f_quad(x; descending=false)都可以。

可以定义有多个关键字参数的函数, 定义时关键字参数与位置参数之间用分号分隔, 关键字参数之间仍使用逗号分隔。 调用时关键字参数与位置参数可以用逗号分隔, 但关键字参数必须使用“关键字名=值”的格式输入。

注意可选参数与关键字参数表面相似, 但是有本质差别:

  • 关键字参数在定义时必须用分号“;”与位置参数分隔;
  • 关键字参数在调用时,必须使用“变量名=变量值”的写法,次序不重要;
  • 所有位置参数,不论是有缺省值的还是没有缺省值的, 都不允许写成“变量名=变量值”的写法, 只能按位置对应, 省略时只能从后向前省略;
  • Julia函数允许同一函数名根据不同的参数个数和类型(signature)而进行不同的操作, 每一个不同的参数组合称为一个“方法”, 这样的特性称为“多重派发”(multiple dispatch), 这样的参数组合只考虑位置参数而不考虑关键字参数。

1.9.4 返回值

函数的最后一个表达式为函数的返回值, 也可以用return y这样的方法返回值。

如果需要不返回任何值, 可以显式地写return nothingnothing表示“不存在”。

如果需要返回多个值, 可以将多个值组成一个元组(tuple)返回, 通过这样的方式就可以返回多个结果。 给元组赋值可以从结果中拆分出多个结果。 如

function summ(x)
  xm = sum(x) / length(x)
  xs = sum(x .^2) / length(x)
  return xm, xs
end
res1, res2 = summ([1, 2, 3, 4, 5])
## (3.0, 11.0)
println(res1, ", ", res2)
## 3.0, 11.0